diff options
Diffstat (limited to 'src/Ryujinx.HLE/HOS/Services/SurfaceFlinger/SurfaceFlinger.cs')
-rw-r--r-- | src/Ryujinx.HLE/HOS/Services/SurfaceFlinger/SurfaceFlinger.cs | 548 |
1 files changed, 548 insertions, 0 deletions
diff --git a/src/Ryujinx.HLE/HOS/Services/SurfaceFlinger/SurfaceFlinger.cs b/src/Ryujinx.HLE/HOS/Services/SurfaceFlinger/SurfaceFlinger.cs new file mode 100644 index 00000000..c7cddf10 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/SurfaceFlinger/SurfaceFlinger.cs @@ -0,0 +1,548 @@ +using Ryujinx.Common.Configuration; +using Ryujinx.Common.Logging; +using Ryujinx.Graphics.GAL; +using Ryujinx.Graphics.Gpu; +using Ryujinx.HLE.HOS.Services.Nv.NvDrvServices.NvMap; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Threading; + +namespace Ryujinx.HLE.HOS.Services.SurfaceFlinger +{ + using ResultCode = Ryujinx.HLE.HOS.Services.Vi.ResultCode; + + class SurfaceFlinger : IConsumerListener, IDisposable + { + private const int TargetFps = 60; + + private Switch _device; + + private Dictionary<long, Layer> _layers; + + private bool _isRunning; + + private Thread _composerThread; + + private Stopwatch _chrono; + + private ManualResetEvent _event = new ManualResetEvent(false); + private AutoResetEvent _nextFrameEvent = new AutoResetEvent(true); + private long _ticks; + private long _ticksPerFrame; + private long _spinTicks; + private long _1msTicks; + + private int _swapInterval; + private int _swapIntervalDelay; + + private readonly object Lock = new object(); + + public long RenderLayerId { get; private set; } + + private class Layer + { + public int ProducerBinderId; + public IGraphicBufferProducer Producer; + public BufferItemConsumer Consumer; + public BufferQueueCore Core; + public ulong Owner; + public LayerState State; + } + + private class TextureCallbackInformation + { + public Layer Layer; + public BufferItem Item; + } + + public SurfaceFlinger(Switch device) + { + _device = device; + _layers = new Dictionary<long, Layer>(); + RenderLayerId = 0; + + _composerThread = new Thread(HandleComposition) + { + Name = "SurfaceFlinger.Composer" + }; + + _chrono = new Stopwatch(); + _chrono.Start(); + + _ticks = 0; + _spinTicks = Stopwatch.Frequency / 500; + _1msTicks = Stopwatch.Frequency / 1000; + + UpdateSwapInterval(1); + + _composerThread.Start(); + } + + private void UpdateSwapInterval(int swapInterval) + { + _swapInterval = swapInterval; + + // If the swap interval is 0, Game VSync is disabled. + if (_swapInterval == 0) + { + _nextFrameEvent.Set(); + _ticksPerFrame = 1; + } + else + { + _ticksPerFrame = Stopwatch.Frequency / TargetFps; + } + } + + public IGraphicBufferProducer CreateLayer(out long layerId, ulong pid, LayerState initialState = LayerState.ManagedClosed) + { + layerId = 1; + + lock (Lock) + { + foreach (KeyValuePair<long, Layer> pair in _layers) + { + if (pair.Key >= layerId) + { + layerId = pair.Key + 1; + } + } + } + + CreateLayerFromId(pid, layerId, initialState); + + return GetProducerByLayerId(layerId); + } + + private void CreateLayerFromId(ulong pid, long layerId, LayerState initialState) + { + lock (Lock) + { + Logger.Info?.Print(LogClass.SurfaceFlinger, $"Creating layer {layerId}"); + + BufferQueueCore core = BufferQueue.CreateBufferQueue(_device, pid, out BufferQueueProducer producer, out BufferQueueConsumer consumer); + + core.BufferQueued += () => + { + _nextFrameEvent.Set(); + }; + + _layers.Add(layerId, new Layer + { + ProducerBinderId = HOSBinderDriverServer.RegisterBinderObject(producer), + Producer = producer, + Consumer = new BufferItemConsumer(_device, consumer, 0, -1, false, this), + Core = core, + Owner = pid, + State = initialState + }); + } + } + + public ResultCode OpenLayer(ulong pid, long layerId, out IBinder producer) + { + Layer layer = GetLayerByIdLocked(layerId); + + if (layer == null || layer.State != LayerState.ManagedClosed) + { + producer = null; + + return ResultCode.InvalidArguments; + } + + layer.State = LayerState.ManagedOpened; + producer = layer.Producer; + + return ResultCode.Success; + } + + public ResultCode CloseLayer(long layerId) + { + lock (Lock) + { + Layer layer = GetLayerByIdLocked(layerId); + + if (layer == null) + { + Logger.Error?.Print(LogClass.SurfaceFlinger, $"Failed to close layer {layerId}"); + + return ResultCode.InvalidValue; + } + + CloseLayer(layerId, layer); + + return ResultCode.Success; + } + } + + public ResultCode DestroyManagedLayer(long layerId) + { + lock (Lock) + { + Layer layer = GetLayerByIdLocked(layerId); + + if (layer == null) + { + Logger.Error?.Print(LogClass.SurfaceFlinger, $"Failed to destroy managed layer {layerId} (not found)"); + + return ResultCode.InvalidValue; + } + + if (layer.State != LayerState.ManagedClosed && layer.State != LayerState.ManagedOpened) + { + Logger.Error?.Print(LogClass.SurfaceFlinger, $"Failed to destroy managed layer {layerId} (permission denied)"); + + return ResultCode.PermissionDenied; + } + + HOSBinderDriverServer.UnregisterBinderObject(layer.ProducerBinderId); + + if (_layers.Remove(layerId) && layer.State == LayerState.ManagedOpened) + { + CloseLayer(layerId, layer); + } + + return ResultCode.Success; + } + } + + public ResultCode DestroyStrayLayer(long layerId) + { + lock (Lock) + { + Layer layer = GetLayerByIdLocked(layerId); + + if (layer == null) + { + Logger.Error?.Print(LogClass.SurfaceFlinger, $"Failed to destroy stray layer {layerId} (not found)"); + + return ResultCode.InvalidValue; + } + + if (layer.State != LayerState.Stray) + { + Logger.Error?.Print(LogClass.SurfaceFlinger, $"Failed to destroy stray layer {layerId} (permission denied)"); + + return ResultCode.PermissionDenied; + } + + HOSBinderDriverServer.UnregisterBinderObject(layer.ProducerBinderId); + + if (_layers.Remove(layerId)) + { + CloseLayer(layerId, layer); + } + + return ResultCode.Success; + } + } + + private void CloseLayer(long layerId, Layer layer) + { + // If the layer was removed and the current in use, we need to change the current layer in use. + if (RenderLayerId == layerId) + { + // If no layer is availaible, reset to default value. + if (_layers.Count == 0) + { + SetRenderLayer(0); + } + else + { + SetRenderLayer(_layers.Last().Key); + } + } + + if (layer.State == LayerState.ManagedOpened) + { + layer.State = LayerState.ManagedClosed; + } + } + + public void SetRenderLayer(long layerId) + { + lock (Lock) + { + RenderLayerId = layerId; + } + } + + private Layer GetLayerByIdLocked(long layerId) + { + foreach (KeyValuePair<long, Layer> pair in _layers) + { + if (pair.Key == layerId) + { + return pair.Value; + } + } + + return null; + } + + public IGraphicBufferProducer GetProducerByLayerId(long layerId) + { + lock (Lock) + { + Layer layer = GetLayerByIdLocked(layerId); + + if (layer != null) + { + return layer.Producer; + } + } + + return null; + } + + private void HandleComposition() + { + _isRunning = true; + + long lastTicks = _chrono.ElapsedTicks; + + while (_isRunning) + { + long ticks = _chrono.ElapsedTicks; + + if (_swapInterval == 0) + { + Compose(); + + _device.System?.SignalVsync(); + + _nextFrameEvent.WaitOne(17); + lastTicks = ticks; + } + else + { + _ticks += ticks - lastTicks; + lastTicks = ticks; + + if (_ticks >= _ticksPerFrame) + { + if (_swapIntervalDelay-- == 0) + { + Compose(); + + // When a frame is presented, delay the next one by its swap interval value. + _swapIntervalDelay = Math.Max(0, _swapInterval - 1); + } + + _device.System?.SignalVsync(); + + // Apply a maximum bound of 3 frames to the tick remainder, in case some event causes Ryujinx to pause for a long time or messes with the timer. + _ticks = Math.Min(_ticks - _ticksPerFrame, _ticksPerFrame * 3); + } + + // Sleep if possible. If the time til the next frame is too low, spin wait instead. + long diff = _ticksPerFrame - (_ticks + _chrono.ElapsedTicks - ticks); + if (diff > 0) + { + if (diff < _spinTicks) + { + do + { + // SpinWait is a little more HT/SMT friendly than aggressively updating/checking ticks. + // The value of 5 still gives us quite a bit of precision (~0.0003ms variance at worst) while waiting a reasonable amount of time. + Thread.SpinWait(5); + + ticks = _chrono.ElapsedTicks; + _ticks += ticks - lastTicks; + lastTicks = ticks; + } while (_ticks < _ticksPerFrame); + } + else + { + _event.WaitOne((int)(diff / _1msTicks)); + } + } + } + } + } + + public void Compose() + { + lock (Lock) + { + // TODO: support multilayers (& multidisplay ?) + if (RenderLayerId == 0) + { + return; + } + + Layer layer = GetLayerByIdLocked(RenderLayerId); + + Status acquireStatus = layer.Consumer.AcquireBuffer(out BufferItem item, 0); + + if (acquireStatus == Status.Success) + { + // If device vsync is disabled, reflect the change. + if (!_device.EnableDeviceVsync) + { + if (_swapInterval != 0) + { + UpdateSwapInterval(0); + } + } + else if (item.SwapInterval != _swapInterval) + { + UpdateSwapInterval(item.SwapInterval); + } + + PostFrameBuffer(layer, item); + } + else if (acquireStatus != Status.NoBufferAvailaible && acquireStatus != Status.InvalidOperation) + { + throw new InvalidOperationException(); + } + } + } + + private void PostFrameBuffer(Layer layer, BufferItem item) + { + int frameBufferWidth = item.GraphicBuffer.Object.Width; + int frameBufferHeight = item.GraphicBuffer.Object.Height; + + int nvMapHandle = item.GraphicBuffer.Object.Buffer.Surfaces[0].NvMapHandle; + + if (nvMapHandle == 0) + { + nvMapHandle = item.GraphicBuffer.Object.Buffer.NvMapId; + } + + ulong bufferOffset = (ulong)item.GraphicBuffer.Object.Buffer.Surfaces[0].Offset; + + NvMapHandle map = NvMapDeviceFile.GetMapFromHandle(layer.Owner, nvMapHandle); + + ulong frameBufferAddress = map.Address + bufferOffset; + + Format format = ConvertColorFormat(item.GraphicBuffer.Object.Buffer.Surfaces[0].ColorFormat); + + int bytesPerPixel = + format == Format.B5G6R5Unorm || + format == Format.R4G4B4A4Unorm ? 2 : 4; + + int gobBlocksInY = 1 << item.GraphicBuffer.Object.Buffer.Surfaces[0].BlockHeightLog2; + + // Note: Rotation is being ignored. + Rect cropRect = item.Crop; + + bool flipX = item.Transform.HasFlag(NativeWindowTransform.FlipX); + bool flipY = item.Transform.HasFlag(NativeWindowTransform.FlipY); + + AspectRatio aspectRatio = _device.Configuration.AspectRatio; + bool isStretched = aspectRatio == AspectRatio.Stretched; + + ImageCrop crop = new ImageCrop( + cropRect.Left, + cropRect.Right, + cropRect.Top, + cropRect.Bottom, + flipX, + flipY, + isStretched, + aspectRatio.ToFloatX(), + aspectRatio.ToFloatY()); + + TextureCallbackInformation textureCallbackInformation = new TextureCallbackInformation + { + Layer = layer, + Item = item + }; + + if (_device.Gpu.Window.EnqueueFrameThreadSafe( + layer.Owner, + frameBufferAddress, + frameBufferWidth, + frameBufferHeight, + 0, + false, + gobBlocksInY, + format, + bytesPerPixel, + crop, + AcquireBuffer, + ReleaseBuffer, + textureCallbackInformation)) + { + if (item.Fence.FenceCount == 0) + { + _device.Gpu.Window.SignalFrameReady(); + _device.Gpu.GPFifo.Interrupt(); + } + else + { + item.Fence.RegisterCallback(_device.Gpu, (x) => + { + _device.Gpu.Window.SignalFrameReady(); + _device.Gpu.GPFifo.Interrupt(); + }); + } + } + else + { + ReleaseBuffer(textureCallbackInformation); + } + } + + private void ReleaseBuffer(object obj) + { + ReleaseBuffer((TextureCallbackInformation)obj); + } + + private void ReleaseBuffer(TextureCallbackInformation information) + { + AndroidFence fence = AndroidFence.NoFence; + + information.Layer.Consumer.ReleaseBuffer(information.Item, ref fence); + } + + private void AcquireBuffer(GpuContext ignored, object obj) + { + AcquireBuffer((TextureCallbackInformation)obj); + } + + private void AcquireBuffer(TextureCallbackInformation information) + { + information.Item.Fence.WaitForever(_device.Gpu); + } + + public static Format ConvertColorFormat(ColorFormat colorFormat) + { + return colorFormat switch + { + ColorFormat.A8B8G8R8 => Format.R8G8B8A8Unorm, + ColorFormat.X8B8G8R8 => Format.R8G8B8A8Unorm, + ColorFormat.R5G6B5 => Format.B5G6R5Unorm, + ColorFormat.A8R8G8B8 => Format.B8G8R8A8Unorm, + ColorFormat.A4B4G4R4 => Format.R4G4B4A4Unorm, + _ => throw new NotImplementedException($"Color Format \"{colorFormat}\" not implemented!"), + }; + } + + public void Dispose() + { + _isRunning = false; + + foreach (Layer layer in _layers.Values) + { + layer.Core.PrepareForExit(); + } + } + + public void OnFrameAvailable(ref BufferItem item) + { + _device.Statistics.RecordGameFrameTime(); + } + + public void OnFrameReplaced(ref BufferItem item) + { + _device.Statistics.RecordGameFrameTime(); + } + + public void OnBuffersReleased() {} + } +} |