using Ryujinx.Common.Logging; using Ryujinx.Common.Memory; using Ryujinx.Graphics.GAL; using Ryujinx.Graphics.Gpu.Image; using Ryujinx.Graphics.Gpu.Memory; using Ryujinx.Graphics.Gpu.Overlay; using Ryujinx.Graphics.Texture; using Ryujinx.Memory.Range; using SkiaSharp; using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Threading; namespace Ryujinx.Graphics.Gpu { /// /// GPU image presentation window. /// public class Window { private readonly GpuContext _context; private readonly OverlayManager _overlayManager; private DateTime _lastUpdateTime = DateTime.UtcNow; /// /// 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 readonly ConcurrentQueue _frameQueue; 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; _overlayManager = new OverlayManager(); _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; } /// /// 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(); texture.SynchronizeMemory(); // Add overlays by modifying texture data directly AddOverlaysToTexture(texture); 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); } } /// /// Add overlay to the overlay manager /// public void AddOverlay(Overlay.Overlay overlay) { _overlayManager.AddOverlay(overlay); } /// /// Add overlays to the texture using SkiaSharp /// /// The texture to modify private void AddOverlaysToTexture(Image.Texture texture) { try { // Calculate delta time for lifespan updates DateTime currentTime = DateTime.UtcNow; float deltaTime = (float)(currentTime - _lastUpdateTime).TotalSeconds; _lastUpdateTime = currentTime; // Update overlay animations _overlayManager.Update(deltaTime, new SKSize(texture.Info.Width, texture.Info.Height)); // Get texture data from host texture using var pinnedData = texture.HostTexture.GetData(); var data = pinnedData.Get().ToArray(); if (data == null || data.Length == 0) return; int width = texture.Info.Width; int height = texture.Info.Height; int bytesPerPixel = texture.Info.FormatInfo.BytesPerPixel; // Determine the SKColorType based on bytes per pixel SKColorType colorType = bytesPerPixel switch { 4 => SKColorType.Rgba8888, 3 => SKColorType.Rgb888x, 2 => SKColorType.Rgb565, _ => SKColorType.Rgba8888 }; // Create SKBitmap from texture data var imageInfo = new SKImageInfo(width, height, colorType, SKAlphaType.Premul); using var bitmap = new SKBitmap(imageInfo); // Copy texture data to bitmap unsafe { fixed (byte* dataPtr = data) { bitmap.SetPixels((IntPtr)dataPtr); } } // Create canvas for drawing overlays using var canvas = new SKCanvas(bitmap); // On Linux with OpenGL, we need to flip the Y-axis because OpenGL uses bottom-left origin // while SkiaSharp uses top-left origin if (OperatingSystem.IsLinux()) { canvas.Scale(1, -1); canvas.Translate(0, -height); } // Render all overlays _overlayManager.Render(canvas); // Copy modified bitmap data back to texture data array var pixels = bitmap.Bytes; if (pixels.Length <= data.Length) { Array.Copy(pixels, data, pixels.Length); } // Upload modified data back to texture var memoryOwner = MemoryOwner.Rent(data.Length); data.CopyTo(memoryOwner.Span); texture.HostTexture.SetData(memoryOwner); // SetData will dispose the MemoryOwner } catch (Exception ex) { // Silently fail if overlay rendering doesn't work Logger.Error?.Print(LogClass.Gpu, $"Overlay rendering failed: {ex.Message}"); } } /// /// 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; } /// /// Dispose resources /// public void Dispose() { _overlayManager?.Dispose(); } } }