From 58b3e2e82b7379ca2fe02072b6e6ddfabcec2678 Mon Sep 17 00:00:00 2001 From: Isaac Marovitz <42140194+IsaacMarovitz@users.noreply.github.com> Date: Wed, 19 Jun 2024 23:13:55 +0100 Subject: [PATCH] Metal: Buffers Take 2 (#21) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Basic BufferManager * Start Scoped Command Buffers * Fences stuff * Remember to cleanup sync manager * Auto, Command Buffer Dependants * Cleanup * Cleanup + Fix Texture->Buffer Copies * Slow buffer upload * Cleanup + Rework TextureBuffer * Don’t get unsafe * Cleanup * Goddamn it * Staging Buffer + Interrupt Action + Flush --- src/Ryujinx.Graphics.Metal/Auto.cs | 146 +++++++++ src/Ryujinx.Graphics.Metal/BitMap.cs | 157 ++++++++++ src/Ryujinx.Graphics.Metal/BufferHolder.cs | 285 +++++++++++++++++ src/Ryujinx.Graphics.Metal/BufferInfo.cs | 11 - src/Ryujinx.Graphics.Metal/BufferManager.cs | 203 ++++++++++++ .../BufferUsageBitmap.cs | 85 +++++ .../CommandBufferPool.cs | 275 ++++++++++++++++ .../CommandBufferScoped.cs | 41 +++ src/Ryujinx.Graphics.Metal/Constants.cs | 2 + .../DisposableBuffer.cs | 26 ++ src/Ryujinx.Graphics.Metal/EncoderState.cs | 7 +- .../EncoderStateManager.cs | 95 +++--- src/Ryujinx.Graphics.Metal/FenceHolder.cs | 77 +++++ src/Ryujinx.Graphics.Metal/Handle.cs | 14 - src/Ryujinx.Graphics.Metal/HelperShader.cs | 2 +- src/Ryujinx.Graphics.Metal/IdList.cs | 121 +++++++ src/Ryujinx.Graphics.Metal/MetalRenderer.cs | 88 +++--- .../MultiFenceHolder.cs | 262 ++++++++++++++++ src/Ryujinx.Graphics.Metal/Pipeline.cs | 86 +++-- src/Ryujinx.Graphics.Metal/StagingBuffer.cs | 294 ++++++++++++++++++ src/Ryujinx.Graphics.Metal/SyncManager.cs | 214 +++++++++++++ src/Ryujinx.Graphics.Metal/Texture.cs | 17 +- src/Ryujinx.Graphics.Metal/TextureBase.cs | 6 +- src/Ryujinx.Graphics.Metal/TextureBuffer.cs | 71 ++--- 24 files changed, 2380 insertions(+), 205 deletions(-) create mode 100644 src/Ryujinx.Graphics.Metal/Auto.cs create mode 100644 src/Ryujinx.Graphics.Metal/BitMap.cs create mode 100644 src/Ryujinx.Graphics.Metal/BufferHolder.cs delete mode 100644 src/Ryujinx.Graphics.Metal/BufferInfo.cs create mode 100644 src/Ryujinx.Graphics.Metal/BufferManager.cs create mode 100644 src/Ryujinx.Graphics.Metal/BufferUsageBitmap.cs create mode 100644 src/Ryujinx.Graphics.Metal/CommandBufferPool.cs create mode 100644 src/Ryujinx.Graphics.Metal/CommandBufferScoped.cs create mode 100644 src/Ryujinx.Graphics.Metal/DisposableBuffer.cs create mode 100644 src/Ryujinx.Graphics.Metal/FenceHolder.cs delete mode 100644 src/Ryujinx.Graphics.Metal/Handle.cs create mode 100644 src/Ryujinx.Graphics.Metal/IdList.cs create mode 100644 src/Ryujinx.Graphics.Metal/MultiFenceHolder.cs create mode 100644 src/Ryujinx.Graphics.Metal/StagingBuffer.cs create mode 100644 src/Ryujinx.Graphics.Metal/SyncManager.cs diff --git a/src/Ryujinx.Graphics.Metal/Auto.cs b/src/Ryujinx.Graphics.Metal/Auto.cs new file mode 100644 index 000000000..8793923a7 --- /dev/null +++ b/src/Ryujinx.Graphics.Metal/Auto.cs @@ -0,0 +1,146 @@ +using System; +using System.Diagnostics; +using System.Runtime.Versioning; +using System.Threading; + +namespace Ryujinx.Graphics.Metal +{ + public interface IAuto + { + bool HasCommandBufferDependency(CommandBufferScoped cbs); + + void IncrementReferenceCount(); + void DecrementReferenceCount(int cbIndex); + void DecrementReferenceCount(); + } + + public interface IAutoPrivate : IAuto + { + void AddCommandBufferDependencies(CommandBufferScoped cbs); + } + + [SupportedOSPlatform("macos")] + public class Auto : IAutoPrivate, IDisposable where T : IDisposable + { + private int _referenceCount; + private T _value; + + private readonly BitMap _cbOwnership; + private readonly MultiFenceHolder _waitable; + + private bool _disposed; + private bool _destroyed; + + public Auto(T value) + { + _referenceCount = 1; + _value = value; + _cbOwnership = new BitMap(CommandBufferPool.MaxCommandBuffers); + } + + public Auto(T value, MultiFenceHolder waitable) : this(value) + { + _waitable = waitable; + } + + public T Get(CommandBufferScoped cbs, int offset, int size, bool write = false) + { + _waitable?.AddBufferUse(cbs.CommandBufferIndex, offset, size, write); + return Get(cbs); + } + + public T GetUnsafe() + { + return _value; + } + + public T Get(CommandBufferScoped cbs) + { + if (!_destroyed) + { + AddCommandBufferDependencies(cbs); + } + + return _value; + } + + public bool HasCommandBufferDependency(CommandBufferScoped cbs) + { + return _cbOwnership.IsSet(cbs.CommandBufferIndex); + } + + public bool HasRentedCommandBufferDependency(CommandBufferPool cbp) + { + return _cbOwnership.AnySet(); + } + + public void AddCommandBufferDependencies(CommandBufferScoped cbs) + { + // We don't want to add a reference to this object to the command buffer + // more than once, so if we detect that the command buffer already has ownership + // of this object, then we can just return without doing anything else. + if (_cbOwnership.Set(cbs.CommandBufferIndex)) + { + if (_waitable != null) + { + cbs.AddWaitable(_waitable); + } + + cbs.AddDependant(this); + } + } + + public bool TryIncrementReferenceCount() + { + int lastValue; + do + { + lastValue = _referenceCount; + + if (lastValue == 0) + { + return false; + } + } + while (Interlocked.CompareExchange(ref _referenceCount, lastValue + 1, lastValue) != lastValue); + + return true; + } + + public void IncrementReferenceCount() + { + if (Interlocked.Increment(ref _referenceCount) == 1) + { + Interlocked.Decrement(ref _referenceCount); + throw new InvalidOperationException("Attempted to increment the reference count of an object that was already destroyed."); + } + } + + public void DecrementReferenceCount(int cbIndex) + { + _cbOwnership.Clear(cbIndex); + DecrementReferenceCount(); + } + + public void DecrementReferenceCount() + { + if (Interlocked.Decrement(ref _referenceCount) == 0) + { + _value.Dispose(); + _value = default; + _destroyed = true; + } + + Debug.Assert(_referenceCount >= 0); + } + + public void Dispose() + { + if (!_disposed) + { + DecrementReferenceCount(); + _disposed = true; + } + } + } +} diff --git a/src/Ryujinx.Graphics.Metal/BitMap.cs b/src/Ryujinx.Graphics.Metal/BitMap.cs new file mode 100644 index 000000000..4ddc438c1 --- /dev/null +++ b/src/Ryujinx.Graphics.Metal/BitMap.cs @@ -0,0 +1,157 @@ +namespace Ryujinx.Graphics.Metal +{ + readonly struct BitMap + { + public const int IntSize = 64; + + private const int IntShift = 6; + private const int IntMask = IntSize - 1; + + private readonly long[] _masks; + + public BitMap(int count) + { + _masks = new long[(count + IntMask) / IntSize]; + } + + public bool AnySet() + { + for (int i = 0; i < _masks.Length; i++) + { + if (_masks[i] != 0) + { + return true; + } + } + + return false; + } + + public bool IsSet(int bit) + { + int wordIndex = bit >> IntShift; + int wordBit = bit & IntMask; + + long wordMask = 1L << wordBit; + + return (_masks[wordIndex] & wordMask) != 0; + } + + public bool IsSet(int start, int end) + { + if (start == end) + { + return IsSet(start); + } + + int startIndex = start >> IntShift; + int startBit = start & IntMask; + long startMask = -1L << startBit; + + int endIndex = end >> IntShift; + int endBit = end & IntMask; + long endMask = (long)(ulong.MaxValue >> (IntMask - endBit)); + + if (startIndex == endIndex) + { + return (_masks[startIndex] & startMask & endMask) != 0; + } + + if ((_masks[startIndex] & startMask) != 0) + { + return true; + } + + for (int i = startIndex + 1; i < endIndex; i++) + { + if (_masks[i] != 0) + { + return true; + } + } + + if ((_masks[endIndex] & endMask) != 0) + { + return true; + } + + return false; + } + + public bool Set(int bit) + { + int wordIndex = bit >> IntShift; + int wordBit = bit & IntMask; + + long wordMask = 1L << wordBit; + + if ((_masks[wordIndex] & wordMask) != 0) + { + return false; + } + + _masks[wordIndex] |= wordMask; + + return true; + } + + public void SetRange(int start, int end) + { + if (start == end) + { + Set(start); + return; + } + + int startIndex = start >> IntShift; + int startBit = start & IntMask; + long startMask = -1L << startBit; + + int endIndex = end >> IntShift; + int endBit = end & IntMask; + long endMask = (long)(ulong.MaxValue >> (IntMask - endBit)); + + if (startIndex == endIndex) + { + _masks[startIndex] |= startMask & endMask; + } + else + { + _masks[startIndex] |= startMask; + + for (int i = startIndex + 1; i < endIndex; i++) + { + _masks[i] |= -1; + } + + _masks[endIndex] |= endMask; + } + } + + public void Clear(int bit) + { + int wordIndex = bit >> IntShift; + int wordBit = bit & IntMask; + + long wordMask = 1L << wordBit; + + _masks[wordIndex] &= ~wordMask; + } + + public void Clear() + { + for (int i = 0; i < _masks.Length; i++) + { + _masks[i] = 0; + } + } + + public void ClearInt(int start, int end) + { + for (int i = start; i <= end; i++) + { + _masks[i] = 0; + } + } + } +} diff --git a/src/Ryujinx.Graphics.Metal/BufferHolder.cs b/src/Ryujinx.Graphics.Metal/BufferHolder.cs new file mode 100644 index 000000000..7fe3530eb --- /dev/null +++ b/src/Ryujinx.Graphics.Metal/BufferHolder.cs @@ -0,0 +1,285 @@ +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(); + } + } +} diff --git a/src/Ryujinx.Graphics.Metal/BufferInfo.cs b/src/Ryujinx.Graphics.Metal/BufferInfo.cs deleted file mode 100644 index b4a1b2cb5..000000000 --- a/src/Ryujinx.Graphics.Metal/BufferInfo.cs +++ /dev/null @@ -1,11 +0,0 @@ -using System; - -namespace Ryujinx.Graphics.Metal -{ - public struct BufferInfo - { - public IntPtr Handle; - public int Offset; - public int Index; - } -} diff --git a/src/Ryujinx.Graphics.Metal/BufferManager.cs b/src/Ryujinx.Graphics.Metal/BufferManager.cs new file mode 100644 index 000000000..8f6c2daa7 --- /dev/null +++ b/src/Ryujinx.Graphics.Metal/BufferManager.cs @@ -0,0 +1,203 @@ +using Ryujinx.Common.Logging; +using Ryujinx.Graphics.GAL; +using SharpMetal.Metal; +using System; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Runtime.Versioning; + +namespace Ryujinx.Graphics.Metal +{ + public readonly struct ScopedTemporaryBuffer : IDisposable + { + private readonly BufferManager _bufferManager; + private readonly bool _isReserved; + + public readonly BufferRange Range; + public readonly BufferHolder Holder; + + public BufferHandle Handle => Range.Handle; + public int Offset => Range.Offset; + + public ScopedTemporaryBuffer(BufferManager bufferManager, BufferHolder holder, BufferHandle handle, int offset, int size, bool isReserved) + { + _bufferManager = bufferManager; + + Range = new BufferRange(handle, offset, size); + Holder = holder; + + _isReserved = isReserved; + } + + public void Dispose() + { + if (!_isReserved) + { + _bufferManager.Delete(Range.Handle); + } + } + } + + [SupportedOSPlatform("macos")] + public class BufferManager : IDisposable + { + private readonly IdList _buffers; + + private readonly MTLDevice _device; + private readonly MetalRenderer _renderer; + private readonly Pipeline _pipeline; + + public int BufferCount { get; private set; } + + public StagingBuffer StagingBuffer { get; } + + public BufferManager(MTLDevice device, MetalRenderer renderer, Pipeline pipeline) + { + _device = device; + _renderer = renderer; + _pipeline = pipeline; + _buffers = new IdList(); + + StagingBuffer = new StagingBuffer(_renderer, _pipeline, this); + } + + public BufferHandle Create(nint pointer, int size) + { + var buffer = _device.NewBuffer(pointer, (ulong)size, MTLResourceOptions.ResourceStorageModeShared); + + if (buffer == IntPtr.Zero) + { + Logger.Error?.PrintMsg(LogClass.Gpu, $"Failed to create buffer with size 0x{size:X}, and pointer 0x{pointer:X}."); + + return BufferHandle.Null; + } + + var holder = new BufferHolder(_renderer, _pipeline, buffer, size); + + BufferCount++; + + ulong handle64 = (uint)_buffers.Add(holder); + + return Unsafe.As(ref handle64); + } + + public BufferHandle CreateWithHandle(int size) + { + return CreateWithHandle(size, out _); + } + + public BufferHandle CreateWithHandle(int size, out BufferHolder holder) + { + holder = Create(size); + + if (holder == null) + { + return BufferHandle.Null; + } + + BufferCount++; + + ulong handle64 = (uint)_buffers.Add(holder); + + return Unsafe.As(ref handle64); + } + + public ScopedTemporaryBuffer ReserveOrCreate(CommandBufferScoped cbs, int size) + { + StagingBufferReserved? result = StagingBuffer.TryReserveData(cbs, size); + + if (result.HasValue) + { + return new ScopedTemporaryBuffer(this, result.Value.Buffer, StagingBuffer.Handle, result.Value.Offset, result.Value.Size, true); + } + else + { + // Create a temporary buffer. + BufferHandle handle = CreateWithHandle(size, out BufferHolder holder); + + return new ScopedTemporaryBuffer(this, holder, handle, 0, size, false); + } + } + + public BufferHolder Create(int size) + { + var buffer = _device.NewBuffer((ulong)size, MTLResourceOptions.ResourceStorageModeShared); + + if (buffer != IntPtr.Zero) + { + return new BufferHolder(_renderer, _pipeline, buffer, size); + } + + Logger.Error?.PrintMsg(LogClass.Gpu, $"Failed to create buffer with size 0x{size:X}."); + + return null; + } + + public Auto GetBuffer(BufferHandle handle, int offset, int size, bool isWrite) + { + if (TryGetBuffer(handle, out var holder)) + { + return holder.GetBuffer(offset, size, isWrite); + } + + return null; + } + + public Auto GetBuffer(BufferHandle handle, bool isWrite) + { + if (TryGetBuffer(handle, out var holder)) + { + return holder.GetBuffer(isWrite); + } + + return null; + } + + public PinnedSpan GetData(BufferHandle handle, int offset, int size) + { + if (TryGetBuffer(handle, out var holder)) + { + return holder.GetData(offset, size); + } + + return new PinnedSpan(); + } + + public void SetData(BufferHandle handle, int offset, ReadOnlySpan data) where T : unmanaged + { + SetData(handle, offset, MemoryMarshal.Cast(data), null, null); + } + + public void SetData(BufferHandle handle, int offset, ReadOnlySpan data, CommandBufferScoped? cbs, Action endRenderPass) + { + if (TryGetBuffer(handle, out var holder)) + { + holder.SetData(offset, data, cbs, endRenderPass); + } + } + + public void Delete(BufferHandle handle) + { + if (TryGetBuffer(handle, out var holder)) + { + holder.Dispose(); + _buffers.Remove((int)Unsafe.As(ref handle)); + } + } + + private bool TryGetBuffer(BufferHandle handle, out BufferHolder holder) + { + return _buffers.TryGetValue((int)Unsafe.As(ref handle), out holder); + } + + public void Dispose() + { + StagingBuffer.Dispose(); + + foreach (var buffer in _buffers) + { + buffer.Dispose(); + } + } + } +} diff --git a/src/Ryujinx.Graphics.Metal/BufferUsageBitmap.cs b/src/Ryujinx.Graphics.Metal/BufferUsageBitmap.cs new file mode 100644 index 000000000..379e27407 --- /dev/null +++ b/src/Ryujinx.Graphics.Metal/BufferUsageBitmap.cs @@ -0,0 +1,85 @@ +using System.Runtime.Versioning; + +namespace Ryujinx.Graphics.Metal +{ + [SupportedOSPlatform("macos")] + internal class BufferUsageBitmap + { + private readonly BitMap _bitmap; + private readonly int _size; + private readonly int _granularity; + private readonly int _bits; + private readonly int _writeBitOffset; + + private readonly int _intsPerCb; + private readonly int _bitsPerCb; + + public BufferUsageBitmap(int size, int granularity) + { + _size = size; + _granularity = granularity; + + // There are two sets of bits - one for read tracking, and the other for write. + int bits = (size + (granularity - 1)) / granularity; + _writeBitOffset = bits; + _bits = bits << 1; + + _intsPerCb = (_bits + (BitMap.IntSize - 1)) / BitMap.IntSize; + _bitsPerCb = _intsPerCb * BitMap.IntSize; + + _bitmap = new BitMap(_bitsPerCb * CommandBufferPool.MaxCommandBuffers); + } + + public void Add(int cbIndex, int offset, int size, bool write) + { + if (size == 0) + { + return; + } + + // Some usages can be out of bounds (vertex buffer on amd), so bound if necessary. + if (offset + size > _size) + { + size = _size - offset; + } + + int cbBase = cbIndex * _bitsPerCb + (write ? _writeBitOffset : 0); + int start = cbBase + offset / _granularity; + int end = cbBase + (offset + size - 1) / _granularity; + + _bitmap.SetRange(start, end); + } + + public bool OverlapsWith(int cbIndex, int offset, int size, bool write = false) + { + if (size == 0) + { + return false; + } + + int cbBase = cbIndex * _bitsPerCb + (write ? _writeBitOffset : 0); + int start = cbBase + offset / _granularity; + int end = cbBase + (offset + size - 1) / _granularity; + + return _bitmap.IsSet(start, end); + } + + public bool OverlapsWith(int offset, int size, bool write) + { + for (int i = 0; i < CommandBufferPool.MaxCommandBuffers; i++) + { + if (OverlapsWith(i, offset, size, write)) + { + return true; + } + } + + return false; + } + + public void Clear(int cbIndex) + { + _bitmap.ClearInt(cbIndex * _intsPerCb, (cbIndex + 1) * _intsPerCb - 1); + } + } +} diff --git a/src/Ryujinx.Graphics.Metal/CommandBufferPool.cs b/src/Ryujinx.Graphics.Metal/CommandBufferPool.cs new file mode 100644 index 000000000..aa659775a --- /dev/null +++ b/src/Ryujinx.Graphics.Metal/CommandBufferPool.cs @@ -0,0 +1,275 @@ +using SharpMetal.Metal; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Runtime.Versioning; + +namespace Ryujinx.Graphics.Metal +{ + [SupportedOSPlatform("macos")] + public class CommandBufferPool : IDisposable + { + public const int MaxCommandBuffers = 16; + + private readonly int _totalCommandBuffers; + private readonly int _totalCommandBuffersMask; + + private readonly MTLDevice _device; + private readonly MTLCommandQueue _queue; + + [SupportedOSPlatform("macos")] + private struct ReservedCommandBuffer + { + public bool InUse; + public bool InConsumption; + public int SubmissionCount; + public MTLCommandBuffer CommandBuffer; + public FenceHolder Fence; + + public List Dependants; + public List Waitables; + + public void Initialize(MTLCommandQueue queue) + { + CommandBuffer = queue.CommandBuffer(); + + Dependants = new List(); + Waitables = new List(); + } + } + + private readonly ReservedCommandBuffer[] _commandBuffers; + + private readonly int[] _queuedIndexes; + private int _queuedIndexesPtr; + private int _queuedCount; + private int _inUseCount; + + public CommandBufferPool(MTLDevice device, MTLCommandQueue queue) + { + _device = device; + _queue = queue; + + _totalCommandBuffers = MaxCommandBuffers; + _totalCommandBuffersMask = _totalCommandBuffers - 1; + + _commandBuffers = new ReservedCommandBuffer[_totalCommandBuffers]; + + _queuedIndexes = new int[_totalCommandBuffers]; + _queuedIndexesPtr = 0; + _queuedCount = 0; + + for (int i = 0; i < _totalCommandBuffers; i++) + { + _commandBuffers[i].Initialize(_queue); + WaitAndDecrementRef(i); + } + } + + public void AddDependant(int cbIndex, IAuto dependant) + { + dependant.IncrementReferenceCount(); + _commandBuffers[cbIndex].Dependants.Add(dependant); + } + + public void AddWaitable(MultiFenceHolder waitable) + { + lock (_commandBuffers) + { + for (int i = 0; i < _totalCommandBuffers; i++) + { + ref var entry = ref _commandBuffers[i]; + + if (entry.InConsumption) + { + AddWaitable(i, waitable); + } + } + } + } + + public void AddInUseWaitable(MultiFenceHolder waitable) + { + lock (_commandBuffers) + { + for (int i = 0; i < _totalCommandBuffers; i++) + { + ref var entry = ref _commandBuffers[i]; + + if (entry.InUse) + { + AddWaitable(i, waitable); + } + } + } + } + + public void AddWaitable(int cbIndex, MultiFenceHolder waitable) + { + ref var entry = ref _commandBuffers[cbIndex]; + if (waitable.AddFence(cbIndex, entry.Fence)) + { + entry.Waitables.Add(waitable); + } + } + + public bool IsFenceOnRentedCommandBuffer(FenceHolder fence) + { + lock (_commandBuffers) + { + for (int i = 0; i < _totalCommandBuffers; i++) + { + ref var entry = ref _commandBuffers[i]; + + if (entry.InUse && entry.Fence == fence) + { + return true; + } + } + } + + return false; + } + + public FenceHolder GetFence(int cbIndex) + { + return _commandBuffers[cbIndex].Fence; + } + + public int GetSubmissionCount(int cbIndex) + { + return _commandBuffers[cbIndex].SubmissionCount; + } + + private int FreeConsumed(bool wait) + { + int freeEntry = 0; + + while (_queuedCount > 0) + { + int index = _queuedIndexes[_queuedIndexesPtr]; + + ref var entry = ref _commandBuffers[index]; + + if (wait || !entry.InConsumption || entry.Fence.IsSignaled()) + { + WaitAndDecrementRef(index); + + wait = false; + freeEntry = index; + + _queuedCount--; + _queuedIndexesPtr = (_queuedIndexesPtr + 1) % _totalCommandBuffers; + } + else + { + break; + } + } + + return freeEntry; + } + + public CommandBufferScoped ReturnAndRent(CommandBufferScoped cbs) + { + Return(cbs); + return Rent(); + } + + public CommandBufferScoped Rent() + { + lock (_commandBuffers) + { + int cursor = FreeConsumed(_inUseCount + _queuedCount == _totalCommandBuffers); + + for (int i = 0; i < _totalCommandBuffers; i++) + { + ref var entry = ref _commandBuffers[cursor]; + + if (!entry.InUse && !entry.InConsumption) + { + entry.InUse = true; + + _inUseCount++; + + return new CommandBufferScoped(this, entry.CommandBuffer, cursor); + } + + cursor = (cursor + 1) & _totalCommandBuffersMask; + } + } + + throw new InvalidOperationException($"Out of command buffers (In use: {_inUseCount}, queued: {_queuedCount}, total: {_totalCommandBuffers})"); + } + + public void Return(CommandBufferScoped cbs) + { + lock (_commandBuffers) + { + int cbIndex = cbs.CommandBufferIndex; + + ref var entry = ref _commandBuffers[cbIndex]; + + Debug.Assert(entry.InUse); + Debug.Assert(entry.CommandBuffer.NativePtr == cbs.CommandBuffer.NativePtr); + entry.InUse = false; + entry.InConsumption = true; + entry.SubmissionCount++; + _inUseCount--; + + var commandBuffer = entry.CommandBuffer; + commandBuffer.Commit(); + + // Replace entry with new MTLCommandBuffer + entry.Initialize(_queue); + + int ptr = (_queuedIndexesPtr + _queuedCount) % _totalCommandBuffers; + _queuedIndexes[ptr] = cbIndex; + _queuedCount++; + } + } + + private void WaitAndDecrementRef(int cbIndex, bool refreshFence = true) + { + ref var entry = ref _commandBuffers[cbIndex]; + + if (entry.InConsumption) + { + entry.Fence.Wait(); + entry.InConsumption = false; + } + + foreach (var dependant in entry.Dependants) + { + dependant.DecrementReferenceCount(cbIndex); + } + + foreach (var waitable in entry.Waitables) + { + waitable.RemoveFence(cbIndex); + waitable.RemoveBufferUses(cbIndex); + } + + entry.Dependants.Clear(); + entry.Waitables.Clear(); + entry.Fence?.Dispose(); + + if (refreshFence) + { + entry.Fence = new FenceHolder(entry.CommandBuffer); + } + else + { + entry.Fence = null; + } + } + + public void Dispose() + { + for (int i = 0; i < _totalCommandBuffers; i++) + { + WaitAndDecrementRef(i, refreshFence: false); + } + } + } +} diff --git a/src/Ryujinx.Graphics.Metal/CommandBufferScoped.cs b/src/Ryujinx.Graphics.Metal/CommandBufferScoped.cs new file mode 100644 index 000000000..48c4d3a1e --- /dev/null +++ b/src/Ryujinx.Graphics.Metal/CommandBufferScoped.cs @@ -0,0 +1,41 @@ +using SharpMetal.Metal; +using System; +using System.Runtime.Versioning; + +namespace Ryujinx.Graphics.Metal +{ + [SupportedOSPlatform("macos")] + public readonly struct CommandBufferScoped : IDisposable + { + private readonly CommandBufferPool _pool; + public MTLCommandBuffer CommandBuffer { get; } + public int CommandBufferIndex { get; } + + public CommandBufferScoped(CommandBufferPool pool, MTLCommandBuffer commandBuffer, int commandBufferIndex) + { + _pool = pool; + CommandBuffer = commandBuffer; + CommandBufferIndex = commandBufferIndex; + } + + public void AddDependant(IAuto dependant) + { + _pool.AddDependant(CommandBufferIndex, dependant); + } + + public void AddWaitable(MultiFenceHolder waitable) + { + _pool.AddWaitable(CommandBufferIndex, waitable); + } + + public FenceHolder GetFence() + { + return _pool.GetFence(CommandBufferIndex); + } + + public void Dispose() + { + _pool?.Return(this); + } + } +} diff --git a/src/Ryujinx.Graphics.Metal/Constants.cs b/src/Ryujinx.Graphics.Metal/Constants.cs index 3f64c6ee3..e1f858a84 100644 --- a/src/Ryujinx.Graphics.Metal/Constants.cs +++ b/src/Ryujinx.Graphics.Metal/Constants.cs @@ -16,5 +16,7 @@ namespace Ryujinx.Graphics.Metal public const int MaxVertexLayouts = 31; public const int MaxTextures = 31; public const int MaxSamplers = 16; + + public const int MinResourceAlignment = 16; } } diff --git a/src/Ryujinx.Graphics.Metal/DisposableBuffer.cs b/src/Ryujinx.Graphics.Metal/DisposableBuffer.cs new file mode 100644 index 000000000..cb7760c1e --- /dev/null +++ b/src/Ryujinx.Graphics.Metal/DisposableBuffer.cs @@ -0,0 +1,26 @@ +using SharpMetal.Metal; +using System; +using System.Runtime.Versioning; + +namespace Ryujinx.Graphics.Metal +{ + [SupportedOSPlatform("macos")] + public readonly struct DisposableBuffer : IDisposable + { + public MTLBuffer Value { get; } + + public DisposableBuffer(MTLBuffer buffer) + { + Value = buffer; + } + + public void Dispose() + { + if (Value != IntPtr.Zero) + { + Value.SetPurgeableState(MTLPurgeableState.Empty); + Value.Dispose(); + } + } + } +} diff --git a/src/Ryujinx.Graphics.Metal/EncoderState.cs b/src/Ryujinx.Graphics.Metal/EncoderState.cs index deee78a60..d0d963ae1 100644 --- a/src/Ryujinx.Graphics.Metal/EncoderState.cs +++ b/src/Ryujinx.Graphics.Metal/EncoderState.cs @@ -1,6 +1,5 @@ using Ryujinx.Graphics.GAL; using SharpMetal.Metal; -using System.Collections.Generic; using System.Linq; using System.Runtime.Versioning; @@ -38,10 +37,10 @@ namespace Ryujinx.Graphics.Metal public TextureBase[] ComputeTextures = new TextureBase[Constants.MaxTextures]; public MTLSamplerState[] ComputeSamplers = new MTLSamplerState[Constants.MaxSamplers]; - public List UniformBuffers = []; - public List StorageBuffers = []; + public BufferAssignment[] UniformBuffers = []; + public BufferAssignment[] StorageBuffers = []; - public MTLBuffer IndexBuffer = default; + public BufferRange IndexBuffer = default; public MTLIndexType IndexType = MTLIndexType.UInt16; public ulong IndexBufferOffset = 0; diff --git a/src/Ryujinx.Graphics.Metal/EncoderStateManager.cs b/src/Ryujinx.Graphics.Metal/EncoderStateManager.cs index 74bdc4258..641d1e2ac 100644 --- a/src/Ryujinx.Graphics.Metal/EncoderStateManager.cs +++ b/src/Ryujinx.Graphics.Metal/EncoderStateManager.cs @@ -4,8 +4,8 @@ using Ryujinx.Graphics.Shader; using SharpMetal.Metal; using System; using System.Collections.Generic; -using System.Runtime.CompilerServices; using System.Runtime.Versioning; +using BufferAssignment = Ryujinx.Graphics.GAL.BufferAssignment; namespace Ryujinx.Graphics.Metal { @@ -13,6 +13,7 @@ namespace Ryujinx.Graphics.Metal struct EncoderStateManager : IDisposable { private readonly Pipeline _pipeline; + private readonly BufferManager _bufferManager; private readonly RenderPipelineCache _renderPipelineCache; private readonly ComputePipelineCache _computePipelineCache; @@ -21,7 +22,7 @@ namespace Ryujinx.Graphics.Metal private EncoderState _currentState = new(); private readonly Stack _backStates = []; - public readonly MTLBuffer IndexBuffer => _currentState.IndexBuffer; + public readonly BufferRange IndexBuffer => _currentState.IndexBuffer; public readonly MTLIndexType IndexType => _currentState.IndexType; public readonly ulong IndexBufferOffset => _currentState.IndexBufferOffset; public readonly PrimitiveTopology Topology => _currentState.Topology; @@ -30,11 +31,13 @@ namespace Ryujinx.Graphics.Metal // RGBA32F is the biggest format private const int ZeroBufferSize = 4 * 4; - private readonly MTLBuffer _zeroBuffer; + private readonly BufferHandle _zeroBuffer; - public unsafe EncoderStateManager(MTLDevice device, Pipeline pipeline) + public unsafe EncoderStateManager(MTLDevice device, BufferManager bufferManager, Pipeline pipeline) { _pipeline = pipeline; + _bufferManager = bufferManager; + _renderPipelineCache = new(device); _computePipelineCache = new(device); _depthStencilCache = new(device); @@ -43,7 +46,7 @@ namespace Ryujinx.Graphics.Metal byte[] zeros = new byte[ZeroBufferSize]; fixed (byte* ptr = zeros) { - _zeroBuffer = device.NewBuffer((IntPtr)ptr, ZeroBufferSize, MTLResourceOptions.ResourceStorageModeShared); + _zeroBuffer = _bufferManager.Create((IntPtr)ptr, ZeroBufferSize); } } @@ -355,8 +358,7 @@ namespace Ryujinx.Graphics.Metal { _currentState.IndexType = type.Convert(); _currentState.IndexBufferOffset = (ulong)buffer.Offset; - var handle = buffer.Handle; - _currentState.IndexBuffer = new(Unsafe.As(ref handle)); + _currentState.IndexBuffer = buffer; } } @@ -657,20 +659,7 @@ namespace Ryujinx.Graphics.Metal // Inlineable public void UpdateUniformBuffers(ReadOnlySpan buffers) { - _currentState.UniformBuffers = []; - - foreach (BufferAssignment buffer in buffers) - { - if (buffer.Range.Size != 0) - { - _currentState.UniformBuffers.Add(new BufferInfo - { - Handle = buffer.Range.Handle.ToIntPtr(), - Offset = buffer.Range.Offset, - Index = buffer.Binding - }); - } - } + _currentState.UniformBuffers = buffers.ToArray(); // Inline update if (_pipeline.CurrentEncoder != null) @@ -691,20 +680,13 @@ namespace Ryujinx.Graphics.Metal // Inlineable public void UpdateStorageBuffers(ReadOnlySpan buffers) { - _currentState.StorageBuffers = []; + _currentState.StorageBuffers = buffers.ToArray(); - foreach (BufferAssignment buffer in buffers) + for (int i = 0; i < _currentState.StorageBuffers.Length; i++) { - if (buffer.Range.Size != 0) - { - // TODO: DONT offset the binding by 15 - _currentState.StorageBuffers.Add(new BufferInfo - { - Handle = buffer.Range.Handle.ToIntPtr(), - Offset = buffer.Range.Offset, - Index = buffer.Binding + 15 - }); - } + BufferAssignment buffer = _currentState.StorageBuffers[i]; + // TODO: DONT offset the binding by 15 + _currentState.StorageBuffers[i] = new BufferAssignment(buffer.Binding + 15, buffer.Range); } // Inline update @@ -956,50 +938,51 @@ namespace Ryujinx.Graphics.Metal private void SetVertexBuffers(MTLRenderCommandEncoder renderCommandEncoder, VertexBufferDescriptor[] bufferDescriptors) { - var buffers = new List(); + var buffers = new List(); for (int i = 0; i < bufferDescriptors.Length; i++) { - if (bufferDescriptors[i].Buffer.Handle.ToIntPtr() != IntPtr.Zero) - { - buffers.Add(new BufferInfo - { - Handle = bufferDescriptors[i].Buffer.Handle.ToIntPtr(), - Offset = bufferDescriptors[i].Buffer.Offset, - Index = i - }); - } + buffers.Add(new BufferAssignment(i, bufferDescriptors[i].Buffer)); } // Zero buffer - buffers.Add(new BufferInfo - { - Handle = _zeroBuffer.NativePtr, - Offset = 0, - Index = bufferDescriptors.Length - }); + buffers.Add(new BufferAssignment( + bufferDescriptors.Length, + new BufferRange(_zeroBuffer, 0, ZeroBufferSize))); - SetRenderBuffers(renderCommandEncoder, buffers); + SetRenderBuffers(renderCommandEncoder, buffers.ToArray()); } - private readonly void SetRenderBuffers(MTLRenderCommandEncoder renderCommandEncoder, List buffers, bool fragment = false) + private readonly void SetRenderBuffers(MTLRenderCommandEncoder renderCommandEncoder, BufferAssignment[] buffers, bool fragment = false) { foreach (var buffer in buffers) { - renderCommandEncoder.SetVertexBuffer(new MTLBuffer(buffer.Handle), (ulong)buffer.Offset, (ulong)buffer.Index); + var range = buffer.Range; + var autoBuffer = _bufferManager.GetBuffer(range.Handle, range.Offset, range.Size, range.Write); - if (fragment) + if (autoBuffer != null) { - renderCommandEncoder.SetFragmentBuffer(new MTLBuffer(buffer.Handle), (ulong)buffer.Offset, (ulong)buffer.Index); + var mtlBuffer = autoBuffer.Get(_pipeline.CurrentCommandBuffer).Value; + + renderCommandEncoder.SetVertexBuffer(mtlBuffer, (ulong)range.Offset, (ulong)buffer.Binding); + + if (fragment) + { + renderCommandEncoder.SetFragmentBuffer(mtlBuffer, (ulong)range.Offset, (ulong)buffer.Binding); + } } } } - private readonly void SetComputeBuffers(MTLComputeCommandEncoder computeCommandEncoder, List buffers) + private readonly void SetComputeBuffers(MTLComputeCommandEncoder computeCommandEncoder, BufferAssignment[] buffers) { foreach (var buffer in buffers) { - computeCommandEncoder.SetBuffer(new MTLBuffer(buffer.Handle), (ulong)buffer.Offset, (ulong)buffer.Index); + var range = buffer.Range; + var mtlBuffer = _bufferManager.GetBuffer(range.Handle, range.Offset, range.Size, range.Write).Get(_pipeline.CurrentCommandBuffer).Value; + + computeCommandEncoder.SetBuffer(mtlBuffer, (ulong)range.Offset, (ulong)buffer.Binding); + } } diff --git a/src/Ryujinx.Graphics.Metal/FenceHolder.cs b/src/Ryujinx.Graphics.Metal/FenceHolder.cs new file mode 100644 index 000000000..3f11fd971 --- /dev/null +++ b/src/Ryujinx.Graphics.Metal/FenceHolder.cs @@ -0,0 +1,77 @@ +using SharpMetal.Metal; +using System; +using System.Runtime.Versioning; +using System.Threading; + +namespace Ryujinx.Graphics.Metal +{ + [SupportedOSPlatform("macos")] + public class FenceHolder : IDisposable + { + private MTLCommandBuffer _fence; + private int _referenceCount; + private bool _disposed; + + public FenceHolder(MTLCommandBuffer fence) + { + _fence = fence; + _referenceCount = 1; + } + + public MTLCommandBuffer GetUnsafe() + { + return _fence; + } + + public bool TryGet(out MTLCommandBuffer fence) + { + int lastValue; + do + { + lastValue = _referenceCount; + + if (lastValue == 0) + { + fence = default; + return false; + } + } while (Interlocked.CompareExchange(ref _referenceCount, lastValue + 1, lastValue) != lastValue); + + fence = _fence; + return true; + } + + public MTLCommandBuffer Get() + { + Interlocked.Increment(ref _referenceCount); + return _fence; + } + + public void Put() + { + if (Interlocked.Decrement(ref _referenceCount) == 0) + { + _fence = default; + } + } + + public void Wait() + { + _fence.WaitUntilCompleted(); + } + + public bool IsSignaled() + { + return _fence.Status == MTLCommandBufferStatus.Completed; + } + + public void Dispose() + { + if (!_disposed) + { + Put(); + _disposed = true; + } + } + } +} diff --git a/src/Ryujinx.Graphics.Metal/Handle.cs b/src/Ryujinx.Graphics.Metal/Handle.cs deleted file mode 100644 index 82f8dbd5b..000000000 --- a/src/Ryujinx.Graphics.Metal/Handle.cs +++ /dev/null @@ -1,14 +0,0 @@ -using Ryujinx.Graphics.GAL; -using System; -using System.Runtime.CompilerServices; - -namespace Ryujinx.Graphics.Metal -{ - static class Handle - { - public static IntPtr ToIntPtr(this BufferHandle handle) - { - return Unsafe.As(ref handle); - } - } -} diff --git a/src/Ryujinx.Graphics.Metal/HelperShader.cs b/src/Ryujinx.Graphics.Metal/HelperShader.cs index 5066b0244..bed199987 100644 --- a/src/Ryujinx.Graphics.Metal/HelperShader.cs +++ b/src/Ryujinx.Graphics.Metal/HelperShader.cs @@ -10,7 +10,7 @@ using System.Runtime.Versioning; namespace Ryujinx.Graphics.Metal { [SupportedOSPlatform("macos")] - class HelperShader : IDisposable + public class HelperShader : IDisposable { private const string ShadersSourcePath = "/Ryujinx.Graphics.Metal/Shaders"; private readonly Pipeline _pipeline; diff --git a/src/Ryujinx.Graphics.Metal/IdList.cs b/src/Ryujinx.Graphics.Metal/IdList.cs new file mode 100644 index 000000000..2c15a80ef --- /dev/null +++ b/src/Ryujinx.Graphics.Metal/IdList.cs @@ -0,0 +1,121 @@ +using System; +using System.Collections.Generic; + +namespace Ryujinx.Graphics.Metal +{ + class IdList where T : class + { + private readonly List _list; + private int _freeMin; + + public IdList() + { + _list = new List(); + _freeMin = 0; + } + + public int Add(T value) + { + int id; + int count = _list.Count; + id = _list.IndexOf(null, _freeMin); + + if ((uint)id < (uint)count) + { + _list[id] = value; + } + else + { + id = count; + _freeMin = id + 1; + + _list.Add(value); + } + + return id + 1; + } + + public void Remove(int id) + { + id--; + + int count = _list.Count; + + if ((uint)id >= (uint)count) + { + return; + } + + if (id + 1 == count) + { + // Trim unused items. + int removeIndex = id; + + while (removeIndex > 0 && _list[removeIndex - 1] == null) + { + removeIndex--; + } + + _list.RemoveRange(removeIndex, count - removeIndex); + + if (_freeMin > removeIndex) + { + _freeMin = removeIndex; + } + } + else + { + _list[id] = null; + + if (_freeMin > id) + { + _freeMin = id; + } + } + } + + public bool TryGetValue(int id, out T value) + { + id--; + + try + { + if ((uint)id < (uint)_list.Count) + { + value = _list[id]; + return value != null; + } + + value = null; + return false; + } + catch (ArgumentOutOfRangeException) + { + value = null; + return false; + } + catch (IndexOutOfRangeException) + { + value = null; + return false; + } + } + + public void Clear() + { + _list.Clear(); + _freeMin = 0; + } + + public IEnumerator GetEnumerator() + { + for (int i = 0; i < _list.Count; i++) + { + if (_list[i] != null) + { + yield return _list[i]; + } + } + } + } +} diff --git a/src/Ryujinx.Graphics.Metal/MetalRenderer.cs b/src/Ryujinx.Graphics.Metal/MetalRenderer.cs index f9ed0423b..1f61090f0 100644 --- a/src/Ryujinx.Graphics.Metal/MetalRenderer.cs +++ b/src/Ryujinx.Graphics.Metal/MetalRenderer.cs @@ -2,11 +2,9 @@ using Ryujinx.Common.Configuration; using Ryujinx.Common.Logging; using Ryujinx.Graphics.GAL; using Ryujinx.Graphics.Shader.Translation; -using SharpMetal.Foundation; using SharpMetal.Metal; using SharpMetal.QuartzCore; using System; -using System.Runtime.CompilerServices; using System.Runtime.Versioning; namespace Ryujinx.Graphics.Metal @@ -19,12 +17,20 @@ namespace Ryujinx.Graphics.Metal private readonly Func _getMetalLayer; private Pipeline _pipeline; + private HelperShader _helperShader; + private BufferManager _bufferManager; private Window _window; + private CommandBufferPool _commandBufferPool; public event EventHandler ScreenCaptured; public bool PreferThreading => true; public IPipeline Pipeline => _pipeline; public IWindow Window => _window; + public HelperShader HelperShader => _helperShader; + public BufferManager BufferManager => _bufferManager; + public CommandBufferPool CommandBufferPool => _commandBufferPool; + public Action InterruptAction { get; private set; } + public SyncManager SyncManager { get; private set; } public MetalRenderer(Func metalLayer) { @@ -35,7 +41,7 @@ namespace Ryujinx.Graphics.Metal throw new NotSupportedException("Metal backend requires Tier 2 Argument Buffer support."); } - _queue = _device.NewCommandQueue(); + _queue = _device.NewCommandQueue(CommandBufferPool.MaxCommandBuffers); _getMetalLayer = metalLayer; } @@ -45,8 +51,15 @@ namespace Ryujinx.Graphics.Metal layer.Device = _device; layer.FramebufferOnly = false; + _commandBufferPool = new CommandBufferPool(_device, _queue); _window = new Window(this, layer); - _pipeline = new Pipeline(_device, _queue); + _pipeline = new Pipeline(_device, this, _queue); + _bufferManager = new BufferManager(_device, this, _pipeline); + + _pipeline.InitEncoderStateManager(_bufferManager); + + _helperShader = new HelperShader(_device, _pipeline); + SyncManager = new SyncManager(this); } public void BackgroundContextAction(Action action, bool alwaysBackground = false) @@ -54,11 +67,14 @@ namespace Ryujinx.Graphics.Metal Logger.Warning?.Print(LogClass.Gpu, "Not Implemented!"); } + public BufferHandle CreateBuffer(int size, BufferAccess access) + { + return _bufferManager.CreateWithHandle(size); + } + public BufferHandle CreateBuffer(IntPtr pointer, int size) { - var buffer = _device.NewBuffer(pointer, (ulong)size, MTLResourceOptions.ResourceStorageModeShared); - var bufferPtr = buffer.NativePtr; - return Unsafe.As(ref bufferPtr); + return _bufferManager.Create(pointer, size); } public BufferHandle CreateBufferSparse(ReadOnlySpan storageBuffers) @@ -71,15 +87,6 @@ namespace Ryujinx.Graphics.Metal throw new NotImplementedException(); } - public BufferHandle CreateBuffer(int size, BufferAccess access) - { - var buffer = _device.NewBuffer((ulong)size, MTLResourceOptions.ResourceStorageModeShared); - buffer.SetPurgeableState(MTLPurgeableState.NonVolatile); - - var bufferPtr = buffer.NativePtr; - return Unsafe.As(ref bufferPtr); - } - public IProgram CreateProgram(ShaderSource[] shaders, ShaderInfo info) { return new Program(shaders, _device); @@ -94,10 +101,10 @@ namespace Ryujinx.Graphics.Metal { if (info.Target == Target.TextureBuffer) { - return new TextureBuffer(_device, _pipeline, info); + return new TextureBuffer(this, info); } - return new Texture(_device, _pipeline, info); + return new Texture(_device, this, _pipeline, info); } public ITextureArray CreateTextureArray(int size, bool isBuffer) @@ -113,19 +120,17 @@ namespace Ryujinx.Graphics.Metal public void CreateSync(ulong id, bool strict) { - Logger.Warning?.Print(LogClass.Gpu, "Not Implemented!"); + SyncManager.Create(id, strict); } public void DeleteBuffer(BufferHandle buffer) { - MTLBuffer mtlBuffer = new(Unsafe.As(ref buffer)); - mtlBuffer.SetPurgeableState(MTLPurgeableState.Empty); + _bufferManager.Delete(buffer); } - public unsafe PinnedSpan GetBufferData(BufferHandle buffer, int offset, int size) + public PinnedSpan GetBufferData(BufferHandle buffer, int offset, int size) { - MTLBuffer mtlBuffer = new(Unsafe.As(ref buffer)); - return new PinnedSpan(IntPtr.Add(mtlBuffer.Contents, offset).ToPointer(), size); + return _bufferManager.GetData(buffer, offset, size); } public Capabilities GetCapabilities() @@ -198,8 +203,7 @@ namespace Ryujinx.Graphics.Metal public ulong GetCurrentSync() { - Logger.Warning?.Print(LogClass.Gpu, "Not Implemented!"); - return 0; + return SyncManager.GetCurrent(); } public HardwareInfo GetHardwareInfo() @@ -212,18 +216,9 @@ namespace Ryujinx.Graphics.Metal throw new NotImplementedException(); } - public unsafe void SetBufferData(BufferHandle buffer, int offset, ReadOnlySpan data) + public void SetBufferData(BufferHandle buffer, int offset, ReadOnlySpan data) { - var blitEncoder = _pipeline.GetOrCreateBlitEncoder(); - - using MTLBuffer src = _device.NewBuffer((ulong)data.Length, MTLResourceOptions.ResourceStorageModeManaged); - { - var span = new Span(src.Contents.ToPointer(), data.Length); - data.CopyTo(span); - - MTLBuffer dst = new(Unsafe.As(ref buffer)); - blitEncoder.CopyFromBuffer(src, 0, dst, (ulong)offset, (ulong)data.Length); - } + _bufferManager.SetData(buffer, offset, data, _pipeline.CurrentCommandBuffer, _pipeline.EndRenderPassDelegate); } public void UpdateCounters() @@ -233,7 +228,7 @@ namespace Ryujinx.Graphics.Metal public void PreFrame() { - + SyncManager.Cleanup(); } public ICounterEvent ReportCounter(CounterType type, EventHandler resultHandler, float divisor, bool hostReserved) @@ -251,12 +246,25 @@ namespace Ryujinx.Graphics.Metal public void WaitSync(ulong id) { - throw new NotImplementedException(); + SyncManager.Wait(id); + } + + public void FlushAllCommands() + { + _pipeline.FlushCommandsImpl(); + } + + public void RegisterFlush() + { + SyncManager.RegisterFlush(); + + // Periodically free unused regions of the staging buffer to avoid doing it all at once. + _bufferManager.StagingBuffer.FreeCompleted(); } public void SetInterruptAction(Action interruptAction) { - // Not needed for now + InterruptAction = interruptAction; } public void Screenshot() diff --git a/src/Ryujinx.Graphics.Metal/MultiFenceHolder.cs b/src/Ryujinx.Graphics.Metal/MultiFenceHolder.cs new file mode 100644 index 000000000..481580b8e --- /dev/null +++ b/src/Ryujinx.Graphics.Metal/MultiFenceHolder.cs @@ -0,0 +1,262 @@ +using SharpMetal.Metal; +using System; +using System.Runtime.Versioning; + +namespace Ryujinx.Graphics.Metal +{ + /// + /// Holder for multiple host GPU fences. + /// + [SupportedOSPlatform("macos")] + public class MultiFenceHolder + { + private const int BufferUsageTrackingGranularity = 4096; + + private readonly FenceHolder[] _fences; + private readonly BufferUsageBitmap _bufferUsageBitmap; + + /// + /// Creates a new instance of the multiple fence holder. + /// + public MultiFenceHolder() + { + _fences = new FenceHolder[CommandBufferPool.MaxCommandBuffers]; + } + + /// + /// Creates a new instance of the multiple fence holder, with a given buffer size in mind. + /// + /// Size of the buffer + public MultiFenceHolder(int size) + { + _fences = new FenceHolder[CommandBufferPool.MaxCommandBuffers]; + _bufferUsageBitmap = new BufferUsageBitmap(size, BufferUsageTrackingGranularity); + } + + /// + /// Adds read/write buffer usage information to the uses list. + /// + /// Index of the command buffer where the buffer is used + /// Offset of the buffer being used + /// Size of the buffer region being used, in bytes + /// Whether the access is a write or not + public void AddBufferUse(int cbIndex, int offset, int size, bool write) + { + _bufferUsageBitmap.Add(cbIndex, offset, size, false); + + if (write) + { + _bufferUsageBitmap.Add(cbIndex, offset, size, true); + } + } + + /// + /// Removes all buffer usage information for a given command buffer. + /// + /// Index of the command buffer where the buffer is used + public void RemoveBufferUses(int cbIndex) + { + _bufferUsageBitmap?.Clear(cbIndex); + } + + /// + /// Checks if a given range of a buffer is being used by a command buffer still being processed by the GPU. + /// + /// Index of the command buffer where the buffer is used + /// Offset of the buffer being used + /// Size of the buffer region being used, in bytes + /// True if in use, false otherwise + public bool IsBufferRangeInUse(int cbIndex, int offset, int size) + { + return _bufferUsageBitmap.OverlapsWith(cbIndex, offset, size); + } + + /// + /// Checks if a given range of a buffer is being used by any command buffer still being processed by the GPU. + /// + /// Offset of the buffer being used + /// Size of the buffer region being used, in bytes + /// True if only write usages should count + /// True if in use, false otherwise + public bool IsBufferRangeInUse(int offset, int size, bool write) + { + return _bufferUsageBitmap.OverlapsWith(offset, size, write); + } + + /// + /// Adds a fence to the holder. + /// + /// Command buffer index of the command buffer that owns the fence + /// Fence to be added + /// True if the command buffer's previous fence value was null + public bool AddFence(int cbIndex, FenceHolder fence) + { + ref FenceHolder fenceRef = ref _fences[cbIndex]; + + if (fenceRef == null) + { + fenceRef = fence; + return true; + } + + return false; + } + + /// + /// Removes a fence from the holder. + /// + /// Command buffer index of the command buffer that owns the fence + public void RemoveFence(int cbIndex) + { + _fences[cbIndex] = null; + } + + /// + /// Determines if a fence referenced on the given command buffer. + /// + /// Index of the command buffer to check if it's used + /// True if referenced, false otherwise + public bool HasFence(int cbIndex) + { + return _fences[cbIndex] != null; + } + + /// + /// Wait until all the fences on the holder are signaled. + /// + public void WaitForFences() + { + WaitForFencesImpl(0, 0, true); + } + + /// + /// Wait until all the fences on the holder with buffer uses overlapping the specified range are signaled. + /// + /// Start offset of the buffer range + /// Size of the buffer range in bytes + public void WaitForFences(int offset, int size) + { + WaitForFencesImpl(offset, size, true); + } + + /// + /// Wait until all the fences on the holder with buffer uses overlapping the specified range are signaled. + /// + + // TODO: Add a proper timeout! + public bool WaitForFences(bool indefinite) + { + return WaitForFencesImpl(0, 0, indefinite); + } + + /// + /// Wait until all the fences on the holder with buffer uses overlapping the specified range are signaled. + /// + /// Start offset of the buffer range + /// Size of the buffer range in bytes + /// Indicates if this should wait indefinitely + /// True if all fences were signaled before the timeout expired, false otherwise + private bool WaitForFencesImpl(int offset, int size, bool indefinite) + { + Span fenceHolders = new FenceHolder[CommandBufferPool.MaxCommandBuffers]; + + int count = size != 0 ? GetOverlappingFences(fenceHolders, offset, size) : GetFences(fenceHolders); + Span fences = stackalloc MTLCommandBuffer[count]; + + int fenceCount = 0; + + for (int i = 0; i < count; i++) + { + if (fenceHolders[i].TryGet(out MTLCommandBuffer fence)) + { + fences[fenceCount] = fence; + + if (fenceCount < i) + { + fenceHolders[fenceCount] = fenceHolders[i]; + } + + fenceCount++; + } + } + + if (fenceCount == 0) + { + return true; + } + + bool signaled = true; + + if (indefinite) + { + foreach (var fence in fences) + { + fence.WaitUntilCompleted(); + } + } + else + { + foreach (var fence in fences) + { + if (fence.Status != MTLCommandBufferStatus.Completed) + { + signaled = false; + } + } + } + + for (int i = 0; i < fenceCount; i++) + { + fenceHolders[i].Put(); + } + + return signaled; + } + + /// + /// Gets fences to wait for. + /// + /// Span to store fences in + /// Number of fences placed in storage + private int GetFences(Span storage) + { + int count = 0; + + for (int i = 0; i < _fences.Length; i++) + { + var fence = _fences[i]; + + if (fence != null) + { + storage[count++] = fence; + } + } + + return count; + } + + /// + /// Gets fences to wait for use of a given buffer region. + /// + /// Span to store overlapping fences in + /// Offset of the range + /// Size of the range in bytes + /// Number of fences for the specified region placed in storage + private int GetOverlappingFences(Span storage, int offset, int size) + { + int count = 0; + + for (int i = 0; i < _fences.Length; i++) + { + var fence = _fences[i]; + + if (fence != null && _bufferUsageBitmap.OverlapsWith(i, offset, size)) + { + storage[count++] = fence; + } + } + + return count; + } + } +} diff --git a/src/Ryujinx.Graphics.Metal/Pipeline.cs b/src/Ryujinx.Graphics.Metal/Pipeline.cs index 34c4d30cb..9e2e4f26f 100644 --- a/src/Ryujinx.Graphics.Metal/Pipeline.cs +++ b/src/Ryujinx.Graphics.Metal/Pipeline.cs @@ -5,12 +5,11 @@ using SharpMetal.Foundation; using SharpMetal.Metal; using SharpMetal.QuartzCore; using System; -using System.Runtime.CompilerServices; using System.Runtime.Versioning; namespace Ryujinx.Graphics.Metal { - enum EncoderType + public enum EncoderType { Blit, Compute, @@ -19,14 +18,19 @@ namespace Ryujinx.Graphics.Metal } [SupportedOSPlatform("macos")] - class Pipeline : IPipeline, IDisposable + public class Pipeline : IPipeline, IDisposable { private readonly MTLDevice _device; private readonly MTLCommandQueue _commandQueue; - private readonly HelperShader _helperShader; + private readonly MetalRenderer _renderer; - private MTLCommandBuffer _commandBuffer; - public MTLCommandBuffer CommandBuffer => _commandBuffer; + private CommandBufferScoped Cbs; + private CommandBufferScoped? PreloadCbs; + public MTLCommandBuffer CommandBuffer; + + public readonly Action EndRenderPassDelegate; + + public CommandBufferScoped CurrentCommandBuffer => Cbs; private MTLCommandEncoder? _currentEncoder; public MTLCommandEncoder? CurrentEncoder => _currentEncoder; @@ -36,14 +40,20 @@ namespace Ryujinx.Graphics.Metal private EncoderStateManager _encoderStateManager; - public Pipeline(MTLDevice device, MTLCommandQueue commandQueue) + public Pipeline(MTLDevice device, MetalRenderer renderer, MTLCommandQueue commandQueue) { _device = device; + _renderer = renderer; _commandQueue = commandQueue; - _helperShader = new HelperShader(_device, this); - _commandBuffer = _commandQueue.CommandBuffer(); - _encoderStateManager = new EncoderStateManager(_device, this); + EndRenderPassDelegate = EndCurrentPass; + + CommandBuffer = (Cbs = _renderer.CommandBufferPool.Rent()).CommandBuffer; + } + + public void InitEncoderStateManager(BufferManager bufferManager) + { + _encoderStateManager = new EncoderStateManager(_device, bufferManager, this); } public void SaveState() @@ -156,7 +166,7 @@ namespace Ryujinx.Graphics.Metal EndCurrentPass(); var descriptor = new MTLBlitPassDescriptor(); - var blitCommandEncoder = _commandBuffer.BlitCommandEncoder(descriptor); + var blitCommandEncoder = Cbs.CommandBuffer.BlitCommandEncoder(descriptor); _currentEncoder = blitCommandEncoder; _currentEncoderType = EncoderType.Blit; @@ -178,21 +188,35 @@ namespace Ryujinx.Graphics.Metal { // TODO: Clean this up var textureInfo = new TextureCreateInfo((int)drawable.Texture.Width, (int)drawable.Texture.Height, (int)drawable.Texture.Depth, (int)drawable.Texture.MipmapLevelCount, (int)drawable.Texture.SampleCount, 0, 0, 0, Format.B8G8R8A8Unorm, 0, Target.Texture2D, SwizzleComponent.Red, SwizzleComponent.Green, SwizzleComponent.Blue, SwizzleComponent.Alpha); - var dst = new Texture(_device, this, textureInfo, drawable.Texture, 0, 0); + var dst = new Texture(_device, _renderer, this, textureInfo, drawable.Texture, 0, 0); - _helperShader.BlitColor(src, dst, srcRegion, dstRegion, isLinear); + _renderer.HelperShader.BlitColor(src, dst, srcRegion, dstRegion, isLinear); EndCurrentPass(); - _commandBuffer.PresentDrawable(drawable); - _commandBuffer.Commit(); + Cbs.CommandBuffer.PresentDrawable(drawable); - _commandBuffer = _commandQueue.CommandBuffer(); + CommandBuffer = (Cbs = _renderer.CommandBufferPool.ReturnAndRent(Cbs)).CommandBuffer; + + // TODO: Auto flush counting + _renderer.SyncManager.GetAndResetWaitTicks(); // Cleanup dst.Dispose(); } + public void FlushCommandsImpl() + { + SaveState(); + + EndCurrentPass(); + + CommandBuffer = (Cbs = _renderer.CommandBufferPool.ReturnAndRent(Cbs)).CommandBuffer; + _renderer.RegisterFlush(); + + RestoreState(); + } + public void BlitColor( ITexture src, ITexture dst, @@ -200,7 +224,7 @@ namespace Ryujinx.Graphics.Metal Extents2D dstRegion, bool linearFilter) { - _helperShader.BlitColor(src, dst, srcRegion, dstRegion, linearFilter); + _renderer.HelperShader.BlitColor(src, dst, srcRegion, dstRegion, linearFilter); } public void Barrier() @@ -235,9 +259,10 @@ namespace Ryujinx.Graphics.Metal { var blitCommandEncoder = GetOrCreateBlitEncoder(); + var mtlBuffer = _renderer.BufferManager.GetBuffer(destination, offset, size, true).Get(Cbs, offset, size, true).Value; + // Might need a closer look, range's count, lower, and upper bound // must be a multiple of 4 - MTLBuffer mtlBuffer = new(Unsafe.As(ref destination)); blitCommandEncoder.FillBuffer(mtlBuffer, new NSRange { @@ -259,7 +284,7 @@ namespace Ryujinx.Graphics.Metal return; } - _helperShader.ClearColor(index, colors, componentMask, dst.Width, dst.Height); + _renderer.HelperShader.ClearColor(index, colors, componentMask, dst.Width, dst.Height); } public void ClearRenderTargetDepthStencil(int layer, int layerCount, float depthValue, bool depthMask, int stencilValue, int stencilMask) @@ -273,7 +298,7 @@ namespace Ryujinx.Graphics.Metal return; } - _helperShader.ClearDepthStencil(depthValue, depthMask, stencilValue, stencilMask, depthStencil.Width, depthStencil.Height); + _renderer.HelperShader.ClearDepthStencil(depthValue, depthMask, stencilValue, stencilMask, depthStencil.Width, depthStencil.Height); } public void CommandBufferBarrier() @@ -281,19 +306,12 @@ namespace Ryujinx.Graphics.Metal Logger.Warning?.Print(LogClass.Gpu, "Not Implemented!"); } - public void CopyBuffer(BufferHandle source, BufferHandle destination, int srcOffset, int dstOffset, int size) + public void CopyBuffer(BufferHandle src, BufferHandle dst, int srcOffset, int dstOffset, int size) { - var blitCommandEncoder = GetOrCreateBlitEncoder(); + var srcBuffer = _renderer.BufferManager.GetBuffer(src, srcOffset, size, false); + var dstBuffer = _renderer.BufferManager.GetBuffer(dst, dstOffset, size, true); - MTLBuffer sourceBuffer = new(Unsafe.As(ref source)); - MTLBuffer destinationBuffer = new(Unsafe.As(ref destination)); - - blitCommandEncoder.CopyFromBuffer( - sourceBuffer, - (ulong)srcOffset, - destinationBuffer, - (ulong)dstOffset, - (ulong)size); + BufferHolder.Copy(this, Cbs, srcBuffer, dstBuffer, srcOffset, dstOffset, size); } public void DispatchCompute(int groupsX, int groupsY, int groupsZ, int groupSizeX, int groupSizeY, int groupSizeZ) @@ -327,11 +345,13 @@ namespace Ryujinx.Graphics.Metal // TODO: Support topology re-indexing to provide support for TriangleFans var primitiveType = _encoderStateManager.Topology.Convert(); + var indexBuffer = _renderer.BufferManager.GetBuffer(_encoderStateManager.IndexBuffer.Handle, false); + renderCommandEncoder.DrawIndexedPrimitives( primitiveType, (ulong)indexCount, _encoderStateManager.IndexType, - _encoderStateManager.IndexBuffer, + indexBuffer.Get(Cbs, 0, indexCount * sizeof(int)).Value, _encoderStateManager.IndexBufferOffset, (ulong)instanceCount, firstVertex, @@ -368,7 +388,7 @@ namespace Ryujinx.Graphics.Metal public void DrawTexture(ITexture texture, ISampler sampler, Extents2DF srcRegion, Extents2DF dstRegion) { - _helperShader.DrawTexture(texture, sampler, srcRegion, dstRegion); + _renderer.HelperShader.DrawTexture(texture, sampler, srcRegion, dstRegion); } public void SetAlphaTest(bool enable, float reference, CompareOp op) diff --git a/src/Ryujinx.Graphics.Metal/StagingBuffer.cs b/src/Ryujinx.Graphics.Metal/StagingBuffer.cs new file mode 100644 index 000000000..81adcd116 --- /dev/null +++ b/src/Ryujinx.Graphics.Metal/StagingBuffer.cs @@ -0,0 +1,294 @@ +using Ryujinx.Common; +using Ryujinx.Common.Logging; +using Ryujinx.Graphics.GAL; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Runtime.Versioning; + +namespace Ryujinx.Graphics.Metal +{ + public readonly struct StagingBufferReserved + { + public readonly BufferHolder Buffer; + public readonly int Offset; + public readonly int Size; + + public StagingBufferReserved(BufferHolder buffer, int offset, int size) + { + Buffer = buffer; + Offset = offset; + Size = size; + } + } + + [SupportedOSPlatform("macos")] + public class StagingBuffer : IDisposable + { + private const int BufferSize = 32 * 1024 * 1024; + + private int _freeOffset; + private int _freeSize; + + private readonly MetalRenderer _renderer; + private readonly Pipeline _pipeline; + private readonly BufferHolder _buffer; + private readonly int _resourceAlignment; + + public readonly BufferHandle Handle; + + private readonly struct PendingCopy + { + public FenceHolder Fence { get; } + public int Size { get; } + + public PendingCopy(FenceHolder fence, int size) + { + Fence = fence; + Size = size; + fence.Get(); + } + } + + private readonly Queue _pendingCopies; + + public StagingBuffer(MetalRenderer renderer, Pipeline pipeline, BufferManager bufferManager) + { + _renderer = renderer; + _pipeline = pipeline; + + Handle = bufferManager.CreateWithHandle(BufferSize, out _buffer); + _pendingCopies = new Queue(); + _freeSize = BufferSize; + _resourceAlignment = Constants.MinResourceAlignment; + } + + public void PushData(CommandBufferPool cbp, CommandBufferScoped? cbs, Action endRenderPass, BufferHolder dst, int dstOffset, ReadOnlySpan data) + { + bool isRender = cbs != null; + CommandBufferScoped scoped = cbs ?? cbp.Rent(); + + // Must push all data to the buffer. If it can't fit, split it up. + + endRenderPass?.Invoke(); + + while (data.Length > 0) + { + if (_freeSize < data.Length) + { + FreeCompleted(); + } + + while (_freeSize == 0) + { + if (!WaitFreeCompleted(cbp)) + { + if (isRender) + { + _renderer.FlushAllCommands(); + scoped = cbp.Rent(); + isRender = false; + } + else + { + scoped = cbp.ReturnAndRent(scoped); + } + } + } + + int chunkSize = Math.Min(_freeSize, data.Length); + + PushDataImpl(scoped, dst, dstOffset, data[..chunkSize]); + + dstOffset += chunkSize; + data = data[chunkSize..]; + } + + if (!isRender) + { + scoped.Dispose(); + } + } + + private void PushDataImpl(CommandBufferScoped cbs, BufferHolder dst, int dstOffset, ReadOnlySpan data) + { + var srcBuffer = _buffer.GetBuffer(); + var dstBuffer = dst.GetBuffer(dstOffset, data.Length, true); + + int offset = _freeOffset; + int capacity = BufferSize - offset; + if (capacity < data.Length) + { + _buffer.SetDataUnchecked(offset, data[..capacity]); + _buffer.SetDataUnchecked(0, data[capacity..]); + + BufferHolder.Copy(_pipeline, cbs, srcBuffer, dstBuffer, offset, dstOffset, capacity); + BufferHolder.Copy(_pipeline, cbs, srcBuffer, dstBuffer, 0, dstOffset + capacity, data.Length - capacity); + } + else + { + _buffer.SetDataUnchecked(offset, data); + + BufferHolder.Copy(_pipeline, cbs, srcBuffer, dstBuffer, offset, dstOffset, data.Length); + } + + _freeOffset = (offset + data.Length) & (BufferSize - 1); + _freeSize -= data.Length; + Debug.Assert(_freeSize >= 0); + + _pendingCopies.Enqueue(new PendingCopy(cbs.GetFence(), data.Length)); + } + + public bool TryPushData(CommandBufferScoped cbs, Action endRenderPass, BufferHolder dst, int dstOffset, ReadOnlySpan data) + { + if (data.Length > BufferSize) + { + return false; + } + + if (_freeSize < data.Length) + { + FreeCompleted(); + + if (_freeSize < data.Length) + { + return false; + } + } + + endRenderPass?.Invoke(); + + PushDataImpl(cbs, dst, dstOffset, data); + + return true; + } + + private StagingBufferReserved ReserveDataImpl(CommandBufferScoped cbs, int size, int alignment) + { + // Assumes the caller has already determined that there is enough space. + int offset = BitUtils.AlignUp(_freeOffset, alignment); + int padding = offset - _freeOffset; + + int capacity = Math.Min(_freeSize, BufferSize - offset); + int reservedLength = size + padding; + if (capacity < size) + { + offset = 0; // Place at start. + reservedLength += capacity; + } + + _freeOffset = (_freeOffset + reservedLength) & (BufferSize - 1); + _freeSize -= reservedLength; + Debug.Assert(_freeSize >= 0); + + _pendingCopies.Enqueue(new PendingCopy(cbs.GetFence(), reservedLength)); + + return new StagingBufferReserved(_buffer, offset, size); + } + + private int GetContiguousFreeSize(int alignment) + { + int alignedFreeOffset = BitUtils.AlignUp(_freeOffset, alignment); + int padding = alignedFreeOffset - _freeOffset; + + // Free regions: + // - Aligned free offset to end (minimum free size - padding) + // - 0 to _freeOffset + freeSize wrapped (only if free area contains 0) + + int endOffset = (_freeOffset + _freeSize) & (BufferSize - 1); + + return Math.Max( + Math.Min(_freeSize - padding, BufferSize - alignedFreeOffset), + endOffset <= _freeOffset ? Math.Min(_freeSize, endOffset) : 0 + ); + } + + /// + /// Reserve a range on the staging buffer for the current command buffer and upload data to it. + /// + /// Command buffer to reserve the data on + /// The minimum size the reserved data requires + /// The required alignment for the buffer offset + /// The reserved range of the staging buffer + public StagingBufferReserved? TryReserveData(CommandBufferScoped cbs, int size, int alignment) + { + if (size > BufferSize) + { + return null; + } + + // Temporary reserved data cannot be fragmented. + + if (GetContiguousFreeSize(alignment) < size) + { + FreeCompleted(); + + if (GetContiguousFreeSize(alignment) < size) + { + Logger.Debug?.PrintMsg(LogClass.Gpu, $"Staging buffer out of space to reserve data of size {size}."); + return null; + } + } + + return ReserveDataImpl(cbs, size, alignment); + } + + /// + /// Reserve a range on the staging buffer for the current command buffer and upload data to it. + /// Uses the most permissive byte alignment. + /// + /// Command buffer to reserve the data on + /// The minimum size the reserved data requires + /// The reserved range of the staging buffer + public StagingBufferReserved? TryReserveData(CommandBufferScoped cbs, int size) + { + return TryReserveData(cbs, size, _resourceAlignment); + } + + private bool WaitFreeCompleted(CommandBufferPool cbp) + { + if (_pendingCopies.TryPeek(out var pc)) + { + if (!pc.Fence.IsSignaled()) + { + if (cbp.IsFenceOnRentedCommandBuffer(pc.Fence)) + { + return false; + } + + pc.Fence.Wait(); + } + + var dequeued = _pendingCopies.Dequeue(); + Debug.Assert(dequeued.Fence == pc.Fence); + _freeSize += pc.Size; + pc.Fence.Put(); + } + + return true; + } + + public void FreeCompleted() + { + FenceHolder signalledFence = null; + while (_pendingCopies.TryPeek(out var pc) && (pc.Fence == signalledFence || pc.Fence.IsSignaled())) + { + signalledFence = pc.Fence; // Already checked - don't need to do it again. + var dequeued = _pendingCopies.Dequeue(); + Debug.Assert(dequeued.Fence == pc.Fence); + _freeSize += pc.Size; + pc.Fence.Put(); + } + } + + public void Dispose() + { + _renderer.BufferManager.Delete(Handle); + + while (_pendingCopies.TryDequeue(out var pc)) + { + pc.Fence.Put(); + } + } + } +} diff --git a/src/Ryujinx.Graphics.Metal/SyncManager.cs b/src/Ryujinx.Graphics.Metal/SyncManager.cs new file mode 100644 index 000000000..528531575 --- /dev/null +++ b/src/Ryujinx.Graphics.Metal/SyncManager.cs @@ -0,0 +1,214 @@ +using Ryujinx.Common.Logging; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Runtime.Versioning; + +namespace Ryujinx.Graphics.Metal +{ + [SupportedOSPlatform("macos")] + public class SyncManager + { + private class SyncHandle + { + public ulong ID; + public MultiFenceHolder Waitable; + public ulong FlushId; + public bool Signalled; + + public bool NeedsFlush(ulong currentFlushId) + { + return (long)(FlushId - currentFlushId) >= 0; + } + } + + private ulong _firstHandle; + + private readonly MetalRenderer _renderer; + private readonly List _handles; + private ulong _flushId; + private long _waitTicks; + + public SyncManager(MetalRenderer renderer) + { + _renderer = renderer; + _handles = new List(); + } + + public void RegisterFlush() + { + _flushId++; + } + + public void Create(ulong id, bool strict) + { + ulong flushId = _flushId; + MultiFenceHolder waitable = new(); + if (strict || _renderer.InterruptAction == null) + { + _renderer.FlushAllCommands(); + _renderer.CommandBufferPool.AddWaitable(waitable); + } + else + { + // Don't flush commands, instead wait for the current command buffer to finish. + // If this sync is waited on before the command buffer is submitted, interrupt the gpu thread and flush it manually. + + _renderer.CommandBufferPool.AddInUseWaitable(waitable); + } + + SyncHandle handle = new() + { + ID = id, + Waitable = waitable, + FlushId = flushId, + }; + + lock (_handles) + { + _handles.Add(handle); + } + } + + public ulong GetCurrent() + { + lock (_handles) + { + ulong lastHandle = _firstHandle; + + foreach (SyncHandle handle in _handles) + { + lock (handle) + { + if (handle.Waitable == null) + { + continue; + } + + if (handle.ID > lastHandle) + { + bool signaled = handle.Signalled || handle.Waitable.WaitForFences(false); + if (signaled) + { + lastHandle = handle.ID; + handle.Signalled = true; + } + } + } + } + + return lastHandle; + } + } + + public void Wait(ulong id) + { + SyncHandle result = null; + + lock (_handles) + { + if ((long)(_firstHandle - id) > 0) + { + return; // The handle has already been signalled or deleted. + } + + foreach (SyncHandle handle in _handles) + { + if (handle.ID == id) + { + result = handle; + break; + } + } + } + + if (result != null) + { + if (result.Waitable == null) + { + return; + } + + long beforeTicks = Stopwatch.GetTimestamp(); + + if (result.NeedsFlush(_flushId)) + { + _renderer.InterruptAction(() => + { + if (result.NeedsFlush(_flushId)) + { + _renderer.FlushAllCommands(); + } + }); + } + + lock (result) + { + if (result.Waitable == null) + { + return; + } + + bool signaled = result.Signalled || result.Waitable.WaitForFences(false); + + if (!signaled) + { + Logger.Error?.PrintMsg(LogClass.Gpu, $"VK Sync Object {result.ID} failed to signal within 1000ms. Continuing..."); + } + else + { + _waitTicks += Stopwatch.GetTimestamp() - beforeTicks; + result.Signalled = true; + } + } + } + } + + public void Cleanup() + { + // Iterate through handles and remove any that have already been signalled. + + while (true) + { + SyncHandle first = null; + lock (_handles) + { + first = _handles.FirstOrDefault(); + } + + if (first == null || first.NeedsFlush(_flushId)) + { + break; + } + + bool signaled = first.Waitable.WaitForFences(false); + if (signaled) + { + // Delete the sync object. + lock (_handles) + { + lock (first) + { + _firstHandle = first.ID + 1; + _handles.RemoveAt(0); + first.Waitable = null; + } + } + } + else + { + // This sync handle and any following have not been reached yet. + break; + } + } + } + + public long GetAndResetWaitTicks() + { + long result = _waitTicks; + _waitTicks = 0; + + return result; + } + } +} diff --git a/src/Ryujinx.Graphics.Metal/Texture.cs b/src/Ryujinx.Graphics.Metal/Texture.cs index 4ec5773bf..7d56375a6 100644 --- a/src/Ryujinx.Graphics.Metal/Texture.cs +++ b/src/Ryujinx.Graphics.Metal/Texture.cs @@ -3,15 +3,14 @@ using SharpMetal.Foundation; using SharpMetal.Metal; using System; using System.Buffers; -using System.Runtime.CompilerServices; using System.Runtime.Versioning; namespace Ryujinx.Graphics.Metal { [SupportedOSPlatform("macos")] - class Texture : TextureBase, ITexture + public class Texture : TextureBase, ITexture { - public Texture(MTLDevice device, Pipeline pipeline, TextureCreateInfo info) : base(device, pipeline, info) + public Texture(MTLDevice device, MetalRenderer renderer, Pipeline pipeline, TextureCreateInfo info) : base(device, renderer, pipeline, info) { var descriptor = new MTLTextureDescriptor { @@ -38,7 +37,7 @@ namespace Ryujinx.Graphics.Metal _mtlTexture = _device.NewTexture(descriptor); } - public Texture(MTLDevice device, Pipeline pipeline, TextureCreateInfo info, MTLTexture sourceTexture, int firstLayer, int firstLevel) : base(device, pipeline, info) + public Texture(MTLDevice device, MetalRenderer renderer, Pipeline pipeline, TextureCreateInfo info, MTLTexture sourceTexture, int firstLayer, int firstLevel) : base(device, renderer, pipeline, info) { var pixelFormat = FormatTable.GetFormat(Info.Format); var textureType = Info.Target.Convert(); @@ -168,6 +167,9 @@ namespace Ryujinx.Graphics.Metal public void CopyTo(BufferRange range, int layer, int level, int stride) { var blitCommandEncoder = _pipeline.GetOrCreateBlitEncoder(); + var cbs = _pipeline.CurrentCommandBuffer; + + int outSize = Info.GetMipSize(level); ulong bytesPerRow = (ulong)Info.GetMipStride(level); ulong bytesPerImage = 0; @@ -176,8 +178,8 @@ namespace Ryujinx.Graphics.Metal bytesPerImage = bytesPerRow * (ulong)Info.Height; } - var handle = range.Handle; - MTLBuffer mtlBuffer = new(Unsafe.As(ref handle)); + var autoBuffer = _renderer.BufferManager.GetBuffer(range.Handle, true); + var mtlBuffer = autoBuffer.Get(cbs, range.Offset, outSize).Value; blitCommandEncoder.CopyFromTexture( _mtlTexture, @@ -193,7 +195,7 @@ namespace Ryujinx.Graphics.Metal public ITexture CreateView(TextureCreateInfo info, int firstLayer, int firstLevel) { - return new Texture(_device, _pipeline, info, _mtlTexture, firstLayer, firstLevel); + return new Texture(_device, _renderer, _pipeline, info, _mtlTexture, firstLayer, firstLevel); } public PinnedSpan GetData() @@ -215,6 +217,7 @@ namespace Ryujinx.Graphics.Metal unsafe { + var mtlBuffer = _device.NewBuffer(length, MTLResourceOptions.ResourceStorageModeShared); blitCommandEncoder.CopyFromTexture( diff --git a/src/Ryujinx.Graphics.Metal/TextureBase.cs b/src/Ryujinx.Graphics.Metal/TextureBase.cs index 8bd5c9513..4b71ec9f9 100644 --- a/src/Ryujinx.Graphics.Metal/TextureBase.cs +++ b/src/Ryujinx.Graphics.Metal/TextureBase.cs @@ -6,13 +6,14 @@ using System.Runtime.Versioning; namespace Ryujinx.Graphics.Metal { [SupportedOSPlatform("macos")] - abstract class TextureBase : IDisposable + public abstract class TextureBase : IDisposable { private bool _disposed; protected readonly TextureCreateInfo _info; protected readonly Pipeline _pipeline; protected readonly MTLDevice _device; + protected readonly MetalRenderer _renderer; protected MTLTexture _mtlTexture; @@ -21,9 +22,10 @@ namespace Ryujinx.Graphics.Metal public int Height => Info.Height; public int Depth => Info.Depth; - public TextureBase(MTLDevice device, Pipeline pipeline, TextureCreateInfo info) + public TextureBase(MTLDevice device, MetalRenderer renderer, Pipeline pipeline, TextureCreateInfo info) { _device = device; + _renderer = renderer; _pipeline = pipeline; _info = info; } diff --git a/src/Ryujinx.Graphics.Metal/TextureBuffer.cs b/src/Ryujinx.Graphics.Metal/TextureBuffer.cs index 38468a76c..3db1e7c4a 100644 --- a/src/Ryujinx.Graphics.Metal/TextureBuffer.cs +++ b/src/Ryujinx.Graphics.Metal/TextureBuffer.cs @@ -2,33 +2,32 @@ using Ryujinx.Graphics.GAL; using SharpMetal.Metal; using System; using System.Buffers; -using System.Runtime.CompilerServices; using System.Runtime.Versioning; namespace Ryujinx.Graphics.Metal { [SupportedOSPlatform("macos")] - class TextureBuffer : Texture, ITexture + class TextureBuffer : ITexture { - private MTLBuffer? _bufferHandle; + private readonly MetalRenderer _renderer; + + private BufferHandle _bufferHandle; private int _offset; private int _size; - public TextureBuffer(MTLDevice device, Pipeline pipeline, TextureCreateInfo info) : base(device, pipeline, info) { } + private int _bufferCount; - public void CreateView() + public int Width { get; } + public int Height { get; } + + public MTLPixelFormat MtlFormat { get; } + + public TextureBuffer(MetalRenderer renderer, TextureCreateInfo info) { - var descriptor = new MTLTextureDescriptor - { - PixelFormat = FormatTable.GetFormat(Info.Format), - Usage = MTLTextureUsage.ShaderRead | MTLTextureUsage.ShaderWrite, - StorageMode = MTLStorageMode.Shared, - TextureType = Info.Target.Convert(), - Width = (ulong)Info.Width, - Height = (ulong)Info.Height - }; - - _mtlTexture = _bufferHandle.Value.NewTexture(descriptor, (ulong)_offset, (ulong)_size); + _renderer = renderer; + Width = info.Width; + Height = info.Height; + MtlFormat = FormatTable.GetFormat(info.Format); } public void CopyTo(ITexture destination, int firstLayer, int firstLevel) @@ -51,10 +50,9 @@ namespace Ryujinx.Graphics.Metal throw new NotSupportedException(); } - // TODO: Implement this method public PinnedSpan GetData() { - throw new NotImplementedException(); + return _renderer.GetBufferData(_bufferHandle, _offset, _size); } public PinnedSpan GetData(int layer, int level) @@ -67,10 +65,14 @@ namespace Ryujinx.Graphics.Metal throw new NotImplementedException(); } + public void Release() + { + + } + public void SetData(IMemoryOwner data) { - // TODO - //_gd.SetBufferData(_bufferHandle, _offset, data.Memory.Span); + _renderer.SetBufferData(_bufferHandle, _offset, data.Memory.Span); data.Dispose(); } @@ -86,25 +88,20 @@ namespace Ryujinx.Graphics.Metal public void SetStorage(BufferRange buffer) { - if (buffer.Handle != BufferHandle.Null) + if (_bufferHandle == buffer.Handle && + _offset == buffer.Offset && + _size == buffer.Size && + _bufferCount == _renderer.BufferManager.BufferCount) { - var handle = buffer.Handle; - MTLBuffer bufferHandle = new(Unsafe.As(ref handle)); - if (_bufferHandle == bufferHandle && - _offset == buffer.Offset && - _size == buffer.Size) - { - return; - } - - _bufferHandle = bufferHandle; - _offset = buffer.Offset; - _size = buffer.Size; - - Release(); - - CreateView(); + return; } + + _bufferHandle = buffer.Handle; + _offset = buffer.Offset; + _size = buffer.Size; + _bufferCount = _renderer.BufferManager.BufferCount; + + Release(); } } }