using Ryujinx.Graphics.GAL; using SharpMetal.Metal; using System; using System.Runtime.InteropServices; using System.Runtime.Versioning; using System.Threading; namespace Ryujinx.Graphics.Metal { [SupportedOSPlatform("macos")] public class BufferHolder : IDisposable { public int Size { get; } private readonly IntPtr _map; private readonly MetalRenderer _renderer; private readonly Pipeline _pipeline; private readonly MultiFenceHolder _waitable; private readonly Auto _buffer; private readonly ReaderWriterLockSlim _flushLock; private FenceHolder _flushFence; private int _flushWaiting; public BufferHolder(MetalRenderer renderer, Pipeline pipeline, MTLBuffer buffer, int size) { _renderer = renderer; _pipeline = pipeline; _map = buffer.Contents; _waitable = new MultiFenceHolder(size); _buffer = new Auto(new(buffer), _waitable); _flushLock = new ReaderWriterLockSlim(); Size = size; } public Auto GetBuffer() { return _buffer; } public Auto GetBuffer(bool isWrite) { if (isWrite) { SignalWrite(0, Size); } return _buffer; } public Auto GetBuffer(int offset, int size, bool isWrite) { if (isWrite) { SignalWrite(offset, size); } return _buffer; } public void SignalWrite(int offset, int size) { if (offset == 0 && size == Size) { // TODO: Cache converted buffers } else { // TODO: Cache converted buffers } } private void ClearFlushFence() { // Assumes _flushLock is held as writer. if (_flushFence != null) { if (_flushWaiting == 0) { _flushFence.Put(); } _flushFence = null; } } private void WaitForFlushFence() { if (_flushFence == null) { return; } // If storage has changed, make sure the fence has been reached so that the data is in place. _flushLock.ExitReadLock(); _flushLock.EnterWriteLock(); if (_flushFence != null) { var fence = _flushFence; Interlocked.Increment(ref _flushWaiting); // Don't wait in the lock. _flushLock.ExitWriteLock(); fence.Wait(); _flushLock.EnterWriteLock(); if (Interlocked.Decrement(ref _flushWaiting) == 0) { fence.Put(); } _flushFence = null; } // Assumes the _flushLock is held as reader, returns in same state. _flushLock.ExitWriteLock(); _flushLock.EnterReadLock(); } public PinnedSpan GetData(int offset, int size) { _flushLock.EnterReadLock(); WaitForFlushFence(); Span result; if (_map != IntPtr.Zero) { result = GetDataStorage(offset, size); // Need to be careful here, the buffer can't be unmapped while the data is being used. _buffer.IncrementReferenceCount(); _flushLock.ExitReadLock(); return PinnedSpan.UnsafeFromSpan(result, _buffer.DecrementReferenceCount); } throw new InvalidOperationException("The buffer is not mapped"); } public unsafe Span GetDataStorage(int offset, int size) { int mappingSize = Math.Min(size, Size - offset); if (_map != IntPtr.Zero) { return new Span((void*)(_map + offset), mappingSize); } throw new InvalidOperationException("The buffer is not mapped."); } public unsafe void SetData(int offset, ReadOnlySpan data, CommandBufferScoped? cbs = null, Action endRenderPass = null, bool allowCbsWait = true) { int dataSize = Math.Min(data.Length, Size - offset); if (dataSize == 0) { return; } if (_map != IntPtr.Zero) { // If persistently mapped, set the data directly if the buffer is not currently in use. bool isRented = _buffer.HasRentedCommandBufferDependency(_renderer.CommandBufferPool); // If the buffer is rented, take a little more time and check if the use overlaps this handle. bool needsFlush = isRented && _waitable.IsBufferRangeInUse(offset, dataSize, false); if (!needsFlush) { WaitForFences(offset, dataSize); data[..dataSize].CopyTo(new Span((void*)(_map + offset), dataSize)); SignalWrite(offset, dataSize); return; } } if (allowCbsWait) { _renderer.BufferManager.StagingBuffer.PushData(_renderer.CommandBufferPool, cbs, endRenderPass, this, offset, data); } else { bool rentCbs = cbs == null; if (rentCbs) { cbs = _renderer.CommandBufferPool.Rent(); } if (!_renderer.BufferManager.StagingBuffer.TryPushData(cbs.Value, endRenderPass, this, offset, data)) { // Need to do a slow upload. BufferHolder srcHolder = _renderer.BufferManager.Create(dataSize); srcHolder.SetDataUnchecked(0, data); var srcBuffer = srcHolder.GetBuffer(); var dstBuffer = this.GetBuffer(true); Copy(_pipeline, cbs.Value, srcBuffer, dstBuffer, 0, offset, dataSize); srcHolder.Dispose(); } if (rentCbs) { cbs.Value.Dispose(); } } } public unsafe void SetDataUnchecked(int offset, ReadOnlySpan data) { int dataSize = Math.Min(data.Length, Size - offset); if (dataSize == 0) { return; } if (_map != IntPtr.Zero) { data[..dataSize].CopyTo(new Span((void*)(_map + offset), dataSize)); } } public void SetDataUnchecked(int offset, ReadOnlySpan data) where T : unmanaged { SetDataUnchecked(offset, MemoryMarshal.AsBytes(data)); } public static void Copy( Pipeline pipeline, CommandBufferScoped cbs, Auto src, Auto dst, int srcOffset, int dstOffset, int size, bool registerSrcUsage = true) { var srcBuffer = registerSrcUsage ? src.Get(cbs, srcOffset, size).Value : src.GetUnsafe().Value; var dstbuffer = dst.Get(cbs, dstOffset, size, true).Value; pipeline.GetOrCreateBlitEncoder().CopyFromBuffer( srcBuffer, (ulong)srcOffset, dstbuffer, (ulong)dstOffset, (ulong)size); } public void WaitForFences() { _waitable.WaitForFences(); } public void WaitForFences(int offset, int size) { _waitable.WaitForFences(offset, size); } public void Dispose() { _buffer.Dispose(); _flushLock.EnterWriteLock(); ClearFlushFence(); _flushLock.ExitWriteLock(); } } }