using Ryujinx.Graphics.GAL; using Ryujinx.Graphics.Gpu.Image; using Ryujinx.Graphics.Gpu.Memory; using Ryujinx.Graphics.Texture; using Ryujinx.Memory.Range; using System; using System.Collections.Concurrent; using System.Threading; namespace Ryujinx.Graphics.Gpu { using Texture = Image.Texture; public record TextureData(int Width, int Height, byte[] Data); /// /// GPU image presentation window. /// public class Window { private const int CaptureTextureWidth = 1280; private const int CaptureTextureHeight = 720; private readonly GpuContext _context; /// /// Texture presented on the window. /// private readonly struct PresentationTexture { /// /// Texture cache where the texture might be located. /// public TextureCache Cache { get; } /// /// Texture information. /// public TextureInfo Info { get; } /// /// Physical memory locations where the texture data is located. /// public MultiRange Range { get; } /// /// Texture crop region. /// public ImageCrop Crop { get; } /// /// Texture acquire callback. /// public Action AcquireCallback { get; } /// /// Texture release callback. /// public Action ReleaseCallback { get; } /// /// User defined object, passed to the various callbacks. /// public object UserObj { get; } /// /// Creates a new instance of the presentation texture. /// /// Texture cache used to look for the texture to be presented /// Information of the texture to be presented /// Physical memory locations where the texture data is located /// Texture crop region /// Texture acquire callback /// Texture release callback /// User defined object passed to the release callback, can be used to identify the texture public PresentationTexture( TextureCache cache, TextureInfo info, MultiRange range, ImageCrop crop, Action acquireCallback, Action releaseCallback, object userObj) { Cache = cache; Info = info; Range = range; Crop = crop; AcquireCallback = acquireCallback; ReleaseCallback = releaseCallback; UserObj = userObj; } } private class PresentedTexture { public readonly Texture Texture; public readonly ImageCrop Crop; public PresentedTexture(Texture texture, ImageCrop crop) { Texture = texture; Crop = crop; } } private readonly ConcurrentQueue _frameQueue; private PresentedTexture _lastPresentedTexture; private ITexture _captureTexture; private int _framesAvailable; public bool IsFrameAvailable => _framesAvailable != 0; /// /// Creates a new instance of the GPU presentation window. /// /// GPU emulation context public Window(GpuContext context) { _context = context; _frameQueue = new ConcurrentQueue(); } /// /// Enqueues a frame for presentation. /// This method is thread safe and can be called from any thread. /// When the texture is presented and not needed anymore, the release callback is called. /// It's an error to modify the texture after calling this method, before the release callback is called. /// /// Process ID of the process that owns the texture pointed to by /// CPU virtual address of the texture data /// Texture width /// Texture height /// Texture stride for linear texture, should be zero otherwise /// Indicates if the texture is linear, normally false /// GOB blocks in the Y direction, for block linear textures /// Texture format /// Texture format bytes per pixel (must match the format) /// Texture crop region /// Texture acquire callback /// Texture release callback /// User defined object passed to the release callback /// Thrown when is invalid /// True if the frame was added to the queue, false otherwise public bool EnqueueFrameThreadSafe( ulong pid, ulong address, int width, int height, int stride, bool isLinear, int gobBlocksInY, Format format, byte bytesPerPixel, ImageCrop crop, Action acquireCallback, Action releaseCallback, object userObj) { if (!_context.PhysicalMemoryRegistry.TryGetValue(pid, out PhysicalMemory physicalMemory)) { return false; } FormatInfo formatInfo = new(format, 1, 1, bytesPerPixel, 4); TextureInfo info = new( 0UL, width, height, 1, 1, 1, 1, stride, isLinear, gobBlocksInY, 1, 1, Target.Texture2D, formatInfo); int size = SizeCalculator.GetBlockLinearTextureSize( width, height, 1, 1, 1, 1, 1, bytesPerPixel, gobBlocksInY, 1, 1).TotalSize; MultiRange range = new(address, (ulong)size); _frameQueue.Enqueue(new PresentationTexture( physicalMemory.TextureCache, info, range, crop, acquireCallback, releaseCallback, userObj)); return true; } public TextureData GetLastPresentedData() { PresentedTexture pt = Volatile.Read(ref _lastPresentedTexture); if (pt != null) { byte[] inputData = CaptureLastFrame(pt.Texture.HostTexture, pt.Crop); int size = SizeCalculator.GetBlockLinearTextureSize( CaptureTextureWidth, CaptureTextureHeight, 1, 1, 1, 1, 1, 4, 16, 1, 1).TotalSize; byte[] data = new byte[size]; LayoutConverter.ConvertLinearToBlockLinear(data, CaptureTextureWidth, CaptureTextureHeight, CaptureTextureWidth * 4, 4, 16, inputData); return new TextureData(CaptureTextureWidth, CaptureTextureHeight, data); } return new TextureData(0, 0, Array.Empty()); } public TextureData GetLastPresentedDataLinear() { PresentedTexture pt = Volatile.Read(ref _lastPresentedTexture); if (pt != null) { byte[] inputData = CaptureLastFrame(pt.Texture.HostTexture, new ImageCrop()); return new TextureData(pt.Texture.Info.Width, pt.Texture.Info.Height, inputData); } return new TextureData(0, 0, Array.Empty()); } /// /// Presents a texture on the queue. /// If the queue is empty, then no texture is presented. /// /// Callback method to call when a new texture should be presented on the screen public void Present(Action swapBuffersCallback) { _context.AdvanceSequence(); if (_frameQueue.TryDequeue(out PresentationTexture pt)) { pt.AcquireCallback(_context, pt.UserObj); Image.Texture texture = pt.Cache.FindOrCreateTexture(null, TextureSearchFlags.WithUpscale, pt.Info, 0, range: pt.Range); pt.Cache.Tick(); EnsureCaptureTexture(); Volatile.Write(ref _lastPresentedTexture, new PresentedTexture(texture, pt.Crop)); texture.SynchronizeMemory(); ImageCrop crop = new( (int)(pt.Crop.Left * texture.ScaleFactor), (int)MathF.Ceiling(pt.Crop.Right * texture.ScaleFactor), (int)(pt.Crop.Top * texture.ScaleFactor), (int)MathF.Ceiling(pt.Crop.Bottom * texture.ScaleFactor), pt.Crop.FlipX, pt.Crop.FlipY, pt.Crop.IsStretched, pt.Crop.AspectRatioX, pt.Crop.AspectRatioY); if (texture.Info.Width > pt.Info.Width || texture.Info.Height > pt.Info.Height) { int top = crop.Top; int bottom = crop.Bottom; int left = crop.Left; int right = crop.Right; if (top == 0 && bottom == 0) { bottom = Math.Min(texture.Info.Height, pt.Info.Height); } if (left == 0 && right == 0) { right = Math.Min(texture.Info.Width, pt.Info.Width); } crop = new ImageCrop(left, right, top, bottom, crop.FlipX, crop.FlipY, crop.IsStretched, crop.AspectRatioX, crop.AspectRatioY); } _context.Renderer.Window.Present(texture.HostTexture, crop, swapBuffersCallback); pt.ReleaseCallback(pt.UserObj); } } private void EnsureCaptureTexture() { if (_captureTexture == null) { _captureTexture = _context.Renderer.CreateTexture(new TextureCreateInfo( 1280, 720, 1, 1, 1, 1, 1, 4, Format.R8G8B8A8Unorm, DepthStencilMode.Depth, Target.Texture2D, SwizzleComponent.Red, SwizzleComponent.Green, SwizzleComponent.Blue, SwizzleComponent.Alpha)); } } private byte[] CaptureLastFrame(ITexture lastFrame, ImageCrop crop) { int cropLeft, cropRight, cropTop, cropBottom; if (crop.Left == 0 && crop.Right == 0) { cropLeft = 0; cropRight = lastFrame.Width; } else { cropLeft = crop.Left; cropRight = crop.Right; } if (crop.Top == 0 && crop.Bottom == 0) { cropTop = 0; cropBottom = lastFrame.Height; } else { cropTop = crop.Top; cropBottom = crop.Bottom; } int x1, y1, x2, y2; if (crop.FlipX) { x1 = cropRight; x2 = cropLeft; } else { x1 = cropLeft; x2 = cropRight; } if (crop.FlipY) { y1 = cropBottom; y2 = cropTop; } else { y1 = cropTop; y2 = cropBottom; } Extents2D srcRegion = new(x1, y1, x2, y2); Extents2D dstRegion = new(0, 0, CaptureTextureWidth, CaptureTextureHeight); byte[] outputData = null; _context.Renderer.BackgroundContextAction(() => { lastFrame.CopyTo(_captureTexture, srcRegion, dstRegion, true); using var data = _captureTexture.GetData(); outputData = data.Get().ToArray(); }); return outputData; } /// /// Indicate that a frame on the queue is ready to be acquired. /// public void SignalFrameReady() { Interlocked.Increment(ref _framesAvailable); } /// /// Determine if any frames are available, and decrement the available count if there are. /// /// True if a frame is available, false otherwise public bool ConsumeFrameAvailable() { if (Interlocked.CompareExchange(ref _framesAvailable, 0, 0) != 0) { Interlocked.Decrement(ref _framesAvailable); return true; } return false; } } }