Overlay system

This commit is contained in:
Barış Hamil 2025-06-21 00:17:10 +03:00 committed by GreemDev
parent bfd715b607
commit ba250df73d
13 changed files with 1088 additions and 19 deletions

View file

@ -41,5 +41,10 @@ namespace Ryujinx.Graphics.GAL.Multithreading
public void SetScalingFilterLevel(float level) { }
public void SetColorSpacePassthrough(bool colorSpacePassthroughEnabled) { }
/// <summary>
/// Gets the underlying implementation window for direct access
/// </summary>
public IWindow BaseWindow => _impl.Window;
}
}

View file

@ -0,0 +1,244 @@
using SkiaSharp;
using System;
using System.Collections.Generic;
using System.Linq;
using OriginalInputConfig = Ryujinx.Common.Configuration.Hid.InputConfig;
using OriginalPlayerIndex = Ryujinx.Common.Configuration.Hid.PlayerIndex;
using OriginalInputBackendType = Ryujinx.Common.Configuration.Hid.InputBackendType;
namespace Ryujinx.Graphics.Gpu.Overlay
{
/// <summary>
/// Controller overlay that shows controller bindings matching the original AXAML design
/// </summary>
public class ControllerOverlay : Overlay
{
private const float OverlayWidth = 400;
private const float Padding = 24;
private const float Spacing = 16;
private const float PlayerSpacing = 12;
private const float PlayerRowHeight = 32;
private const float TitleTextSize = 25;
private const float PlayerTextSize = 22;
private float _lifespan = 0f;
public ControllerOverlay() : base("ControllerOverlay")
{
CreateBaseElements();
}
private void CreateBaseElements()
{
// Main background container
var background = new RectangleElement(0, 0, OverlayWidth, 200, // Dynamic height will be set later
new SKColor(0, 0, 0, 224)) // #E0000000
{
Name = "Background",
CornerRadius = 12,
BorderColor = new SKColor(255, 255, 255, 64), // #40FFFFFF
BorderWidth = 1
};
AddElement(background);
// Title text
var titleText = new TextElement(Padding + 30, Padding, "Controller Bindings", TitleTextSize, SKColors.White)
{
Name = "TitleText",
FontStyle = SKFontStyle.Bold
};
AddElement(titleText);
}
/// <summary>
/// Show controller bindings matching the original AXAML implementation
/// </summary>
public void ShowControllerBindings(List<OriginalInputConfig> inputConfigs, int durationSeconds = 3)
{
// Reset lifespan and opacity
_lifespan = durationSeconds;
// Clear existing player bindings
ClearPlayerBindings();
// Debug: Log input data
// Group controllers by player index
var playerBindings = new Dictionary<OriginalPlayerIndex, List<OriginalInputConfig>>();
foreach (var config in inputConfigs.Where(c => c.PlayerIndex <= OriginalPlayerIndex.Player4))
{
Console.WriteLine($"ControllerOverlay: Config for Player {config.PlayerIndex}: {config.Name} ({config.Backend})");
if (!playerBindings.ContainsKey(config.PlayerIndex))
{
playerBindings[config.PlayerIndex] = new List<OriginalInputConfig>();
}
playerBindings[config.PlayerIndex].Add(config);
}
float currentY = Padding + 40; // After title
// Add player bindings to UI
for (int i = 0; i < 4; i++)
{
var playerIndex = (OriginalPlayerIndex)i;
float rowY = currentY + (i * (PlayerRowHeight + PlayerSpacing));
// Player number with colored background (circular badge)
var playerColor = GetPlayerColor(i);
var playerBadge = new RectangleElement(Padding, rowY, 24, 20, playerColor)
{
Name = $"PlayerBadge_{i}",
CornerRadius = 12
};
AddElement(playerBadge);
// Player number text
var playerLabel = new TextElement(Padding + 12, rowY + 2, $"P{i + 1}", PlayerTextSize, SKColors.White)
{
Name = $"PlayerLabel_{i}",
FontStyle = SKFontStyle.Bold,
TextAlign = SKTextAlign.Center
};
AddElement(playerLabel);
// Controller info
if (playerBindings.ContainsKey(playerIndex))
{
var controllers = playerBindings[playerIndex];
var controllerNames = controllers.Select(c => GetControllerDisplayName(c)).ToList();
var controllerText = new TextElement(Padding + 56, rowY + 2, string.Join(", ", controllerNames), PlayerTextSize, new SKColor(144, 238, 144)) // LightGreen
{
Name = $"ControllerText_{i}",
FontStyle = SKFontStyle.Bold
};
AddElement(controllerText);
}
else
{
var noControllerText = new TextElement(Padding + 56, rowY + 2, "No controller assigned", PlayerTextSize, new SKColor(128, 128, 128)) // Gray
{
Name = $"NoControllerText_{i}",
FontStyle = SKFontStyle.Italic
};
AddElement(noControllerText);
}
}
// Calculate total height and update background
float totalHeight = Padding + 40 + (4 * (PlayerRowHeight + PlayerSpacing)) + Padding + 40; // Extra space for duration text
var background = FindElement<RectangleElement>("Background");
if (background != null)
{
background.Height = totalHeight;
}
// Duration text at bottom
string durationText = durationSeconds == 1
? "This overlay will disappear in 1 second"
: $"This overlay will disappear in {durationSeconds} seconds";
// Show the overlay (position will be set by Window class with actual dimensions)
IsVisible = true;
}
private static SKColor GetPlayerColor(int playerIndex)
{
return playerIndex switch
{
0 => new SKColor(255, 92, 92), // Red for Player 1
1 => new SKColor(54, 162, 235), // Blue for Player 2
2 => new SKColor(255, 206, 84), // Yellow for Player 3
3 => new SKColor(75, 192, 192), // Green for Player 4
_ => new SKColor(128, 128, 128) // Gray fallback
};
}
private static string GetControllerDisplayName(OriginalInputConfig config)
{
if (string.IsNullOrEmpty(config.Name))
{
return config.Backend switch
{
OriginalInputBackendType.WindowKeyboard => "Keyboard",
OriginalInputBackendType.GamepadSDL2 => "Controller",
_ => "Unknown"
};
}
// Truncate long controller names
string name = config.Name;
if (name.Length > 25)
{
name = name.Substring(0, 22) + "...";
}
return name;
}
/// <summary>
/// Clear all player bindings
/// </summary>
private void ClearPlayerBindings()
{
var elementsToRemove = new List<OverlayElement>();
foreach (var element in GetElements())
{
if (element.Name.StartsWith("PlayerBadge_") ||
element.Name.StartsWith("PlayerLabel_") ||
element.Name.StartsWith("ControllerText_") ||
element.Name.StartsWith("NoControllerText_"))
{
elementsToRemove.Add(element);
}
}
foreach (var element in elementsToRemove)
{
RemoveElement(element);
element.Dispose();
}
}
/// <summary>
/// Update overlay
/// </summary>
public override void Update(float deltaTime, SKSize screenSize)
{
_lifespan -= deltaTime;
if (_lifespan <= 0)
{
IsVisible = false;
return;
}
if (_lifespan <= 0.5f)
{
// Fade out during the last 0.5 seconds
Opacity = _lifespan / 0.5f;
}
else
{
Opacity = 1;
}
// Update position if screen size is provided
if (screenSize.Width > 0 && screenSize.Height > 0)
{
SetPositionToTopRight(screenSize.Width, screenSize.Height);
}
}
/// <summary>
/// Position overlay to top-right matching original AXAML positioning
/// </summary>
public void SetPositionToTopRight(float screenWidth, float screenHeight)
{
X = screenWidth - OverlayWidth - 20; // 20px margin from right
Y = 50; // 50px margin from top
}
}
}

View file

@ -0,0 +1,145 @@
using SkiaSharp;
using System;
namespace Ryujinx.Graphics.Gpu.Overlay
{
/// <summary>
/// Image overlay element
/// </summary>
public class ImageElement : OverlayElement
{
private SKBitmap? _bitmap;
private byte[]? _imageData;
private string? _imagePath;
public SKFilterQuality FilterQuality { get; set; } = SKFilterQuality.Medium;
public bool MaintainAspectRatio { get; set; } = true;
public ImageElement()
{
}
public ImageElement(float x, float y, float width, float height, byte[] imageData)
{
X = x;
Y = y;
Width = width;
Height = height;
SetImageData(imageData);
}
public ImageElement(float x, float y, float width, float height, string imagePath)
{
X = x;
Y = y;
Width = width;
Height = height;
SetImagePath(imagePath);
}
/// <summary>
/// Set image from byte array
/// </summary>
public void SetImageData(byte[] imageData)
{
_imageData = imageData;
_imagePath = null;
LoadBitmap();
}
/// <summary>
/// Set image from file path
/// </summary>
public void SetImagePath(string imagePath)
{
_imagePath = imagePath;
_imageData = null;
LoadBitmap();
}
/// <summary>
/// Set image from existing SKBitmap
/// </summary>
public void SetBitmap(SKBitmap bitmap)
{
_bitmap?.Dispose();
_bitmap = bitmap;
_imageData = null;
_imagePath = null;
}
private void LoadBitmap()
{
try
{
_bitmap?.Dispose();
_bitmap = null;
if (_imageData != null)
{
_bitmap = SKBitmap.Decode(_imageData);
}
else if (!string.IsNullOrEmpty(_imagePath))
{
_bitmap = SKBitmap.Decode(_imagePath);
}
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"Failed to load image: {ex.Message}");
_bitmap = null;
}
}
public override void Render(SKCanvas canvas, float globalOpacity = 1.0f)
{
if (!IsVisible || _bitmap == null || Width <= 0 || Height <= 0)
return;
float effectiveOpacity = Opacity * globalOpacity;
using var paint = new SKPaint
{
FilterQuality = FilterQuality,
Color = SKColors.White.WithAlpha((byte)(255 * effectiveOpacity))
};
var sourceRect = new SKRect(0, 0, _bitmap.Width, _bitmap.Height);
var destRect = new SKRect(X, Y, X + Width, Y + Height);
if (MaintainAspectRatio)
{
// Calculate aspect ratio preserving destination rectangle
float sourceAspect = (float)_bitmap.Width / _bitmap.Height;
float destAspect = Width / Height;
if (sourceAspect > destAspect)
{
// Source is wider, fit to width
float newHeight = Width / sourceAspect;
float yOffset = (Height - newHeight) / 2;
destRect = new SKRect(X, Y + yOffset, X + Width, Y + yOffset + newHeight);
}
else
{
// Source is taller, fit to height
float newWidth = Height * sourceAspect;
float xOffset = (Width - newWidth) / 2;
destRect = new SKRect(X + xOffset, Y, X + xOffset + newWidth, Y + Height);
}
}
canvas.DrawBitmap(_bitmap, sourceRect, destRect, paint);
}
protected override void Dispose(bool disposing)
{
if (disposing)
{
_bitmap?.Dispose();
_bitmap = null;
}
base.Dispose(disposing);
}
}
}

View file

@ -0,0 +1,115 @@
using SkiaSharp;
using System;
using System.Collections.Generic;
using System.Linq;
namespace Ryujinx.Graphics.Gpu.Overlay
{
/// <summary>
/// Base overlay class containing multiple elements
/// </summary>
public abstract class Overlay : IDisposable
{
private readonly List<OverlayElement> _elements = new();
public string Name { get; set; } = string.Empty;
public bool IsVisible { get; set; } = true;
public float Opacity { get; set; } = 1.0f;
public float X { get; set; }
public float Y { get; set; }
public int ZIndex { get; set; } = 0;
public Overlay()
{
}
public Overlay(string name)
{
Name = name;
}
/// <summary>
/// Add an element to this overlay
/// </summary>
public void AddElement(OverlayElement element)
{
_elements.Add(element);
}
/// <summary>
/// Remove an element from this overlay
/// </summary>
public void RemoveElement(OverlayElement element)
{
_elements.Remove(element);
}
/// <summary>
/// Get all elements
/// </summary>
public IReadOnlyList<OverlayElement> GetElements()
{
return _elements.AsReadOnly();
}
/// <summary>
/// Find element by name
/// </summary>
public T FindElement<T>(string name) where T : OverlayElement
{
return _elements.OfType<T>().FirstOrDefault(e => e.Name == name);
}
/// <summary>
/// Clear all elements
/// </summary>
public void Clear()
{
foreach (var element in _elements)
{
element.Dispose();
}
_elements.Clear();
}
/// <summary>
/// Update overlay
/// </summary>
public abstract void Update(float deltaTime, SKSize screenSize = default);
/// <summary>
/// Render this overlay
/// </summary>
public void Render(SKCanvas canvas)
{
if (!IsVisible || Opacity <= 0.0f)
return;
// Save canvas state
canvas.Save();
// Apply overlay position offset
if (X != 0 || Y != 0)
{
canvas.Translate(X, Y);
}
// Render all elements
foreach (var element in _elements)
{
if (element.IsVisible)
{
element.Render(canvas, Opacity);
}
}
// Restore canvas state
canvas.Restore();
}
public void Dispose()
{
Clear();
}
}
}

View file

@ -0,0 +1,66 @@
using SkiaSharp;
using System;
namespace Ryujinx.Graphics.Gpu.Overlay
{
/// <summary>
/// Base class for all overlay elements
/// </summary>
public abstract class OverlayElement : IDisposable
{
public float X { get; set; }
public float Y { get; set; }
public float Width { get; set; }
public float Height { get; set; }
public bool IsVisible { get; set; } = true;
public float Opacity { get; set; } = 1.0f;
public string Name { get; set; } = string.Empty;
/// <summary>
/// Render this element to the canvas
/// </summary>
/// <param name="canvas">The canvas to draw on</param>
/// <param name="globalOpacity">Global opacity multiplier</param>
public abstract void Render(SKCanvas canvas, float globalOpacity = 1.0f);
/// <summary>
/// Check if a point is within this element's bounds
/// </summary>
public virtual bool Contains(float x, float y)
{
return x >= X && x <= X + Width && y >= Y && y <= Y + Height;
}
/// <summary>
/// Get the bounds of this element
/// </summary>
public SKRect GetBounds()
{
return new SKRect(X, Y, X + Width, Y + Height);
}
/// <summary>
/// Apply opacity to a color
/// </summary>
protected SKColor ApplyOpacity(SKColor color, float opacity)
{
return color.WithAlpha((byte)(color.Alpha * opacity));
}
/// <summary>
/// Dispose of resources
/// </summary>
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
/// <summary>
/// Dispose of resources
/// </summary>
protected virtual void Dispose(bool disposing)
{
}
}
}

View file

@ -0,0 +1,160 @@
using SkiaSharp;
using System;
using System.Collections.Generic;
using System.Linq;
using Ryujinx.Graphics.Gpu.Image;
namespace Ryujinx.Graphics.Gpu.Overlay
{
/// <summary>
/// Manages multiple overlays and handles rendering
/// </summary>
public class OverlayManager : IDisposable
{
private readonly List<Overlay> _overlays = new();
private readonly object _lock = new();
/// <summary>
/// Add an overlay to the manager
/// </summary>
public void AddOverlay(Overlay overlay)
{
lock (_lock)
{
_overlays.Add(overlay);
SortOverlays();
}
}
/// <summary>
/// Remove an overlay from the manager
/// </summary>
public void RemoveOverlay(Overlay overlay)
{
lock (_lock)
{
_overlays.Remove(overlay);
}
}
/// <summary>
/// Remove overlay by name
/// </summary>
public void RemoveOverlay(string name)
{
lock (_lock)
{
var overlay = _overlays.FirstOrDefault(o => o.Name == name);
if (overlay != null)
{
_overlays.Remove(overlay);
overlay.Dispose();
}
}
}
/// <summary>
/// Find overlay by name
/// </summary>
public Overlay FindOverlay(string name)
{
lock (_lock)
{
return _overlays.FirstOrDefault(o => o.Name == name);
}
}
/// <summary>
/// Get all overlays
/// </summary>
public IReadOnlyList<Overlay> GetOverlays()
{
lock (_lock)
{
return _overlays.AsReadOnly();
}
}
/// <summary>
/// Clear all overlays
/// </summary>
public void Clear()
{
lock (_lock)
{
foreach (var overlay in _overlays)
{
overlay.Dispose();
}
_overlays.Clear();
}
}
/// <summary>
/// Update all overlays (for animations)
/// </summary>
public void Update(float deltaTime, SKSize screenSize = default)
{
lock (_lock)
{
foreach (var overlay in _overlays.Where(o => o.IsVisible))
{
overlay.Update(deltaTime, screenSize);
}
}
}
/// <summary>
/// Render all visible overlays
/// </summary>
public void Render(SKCanvas canvas)
{
lock (_lock)
{
foreach (var overlay in _overlays.Where(o => o.IsVisible && o.Opacity > 0.0f))
{
overlay.Render(canvas);
}
}
}
/// <summary>
/// Sort overlays by Z-index
/// </summary>
private void SortOverlays()
{
_overlays.Sort((a, b) => a.ZIndex.CompareTo(b.ZIndex));
}
/// <summary>
/// Show overlay
/// </summary>
public void ShowOverlay(string name)
{
var overlay = FindOverlay(name);
if (overlay != null)
{
overlay.IsVisible = true;
overlay.Opacity = 1.0f;
}
}
/// <summary>
/// Hide overlay
/// </summary>
public void HideOverlay(string name)
{
var overlay = FindOverlay(name);
if (overlay != null)
{
overlay.IsVisible = false;
overlay.Opacity = 0.0f;
}
}
public void Dispose()
{
Clear();
}
}
}

View file

@ -0,0 +1,78 @@
using SkiaSharp;
namespace Ryujinx.Graphics.Gpu.Overlay
{
/// <summary>
/// Rectangle overlay element
/// </summary>
public class RectangleElement : OverlayElement
{
public SKColor BackgroundColor { get; set; } = SKColors.Transparent;
public SKColor BorderColor { get; set; } = SKColors.Transparent;
public float BorderWidth { get; set; } = 0;
public float CornerRadius { get; set; } = 0;
public RectangleElement()
{
}
public RectangleElement(float x, float y, float width, float height, SKColor backgroundColor)
{
X = x;
Y = y;
Width = width;
Height = height;
BackgroundColor = backgroundColor;
}
public override void Render(SKCanvas canvas, float globalOpacity = 1.0f)
{
if (!IsVisible || Width <= 0 || Height <= 0)
return;
float effectiveOpacity = Opacity * globalOpacity;
var bounds = new SKRect(X, Y, X + Width, Y + Height);
// Draw background
if (BackgroundColor.Alpha > 0)
{
using var backgroundPaint = new SKPaint
{
Color = ApplyOpacity(BackgroundColor, effectiveOpacity),
Style = SKPaintStyle.Fill,
IsAntialias = true
};
if (CornerRadius > 0)
{
canvas.DrawRoundRect(bounds, CornerRadius, CornerRadius, backgroundPaint);
}
else
{
canvas.DrawRect(bounds, backgroundPaint);
}
}
// Draw border
if (BorderWidth > 0 && BorderColor.Alpha > 0)
{
using var borderPaint = new SKPaint
{
Color = ApplyOpacity(BorderColor, effectiveOpacity),
Style = SKPaintStyle.Stroke,
StrokeWidth = BorderWidth,
IsAntialias = true
};
if (CornerRadius > 0)
{
canvas.DrawRoundRect(bounds, CornerRadius, CornerRadius, borderPaint);
}
else
{
canvas.DrawRect(bounds, borderPaint);
}
}
}
}
}

View file

@ -0,0 +1,121 @@
using SkiaSharp;
namespace Ryujinx.Graphics.Gpu.Overlay
{
/// <summary>
/// Text overlay element
/// </summary>
public class TextElement : OverlayElement
{
public string Text { get; set; } = string.Empty;
public SKColor TextColor { get; set; } = SKColors.White;
public float FontSize { get; set; } = 16;
public string FontFamily { get; set; } = "Arial";
public SKFontStyle FontStyle { get; set; } = SKFontStyle.Normal;
public SKTextAlign TextAlign { get; set; } = SKTextAlign.Left;
public bool IsAntialias { get; set; } = true;
// Shadow properties
public bool HasShadow { get; set; } = false;
public SKColor ShadowColor { get; set; } = SKColors.Black;
public float ShadowOffsetX { get; set; } = 1;
public float ShadowOffsetY { get; set; } = 1;
public float ShadowBlur { get; set; } = 0;
public TextElement()
{
}
public TextElement(float x, float y, string text, float fontSize = 16, SKColor? color = null)
{
X = x;
Y = y;
Text = text;
FontSize = fontSize;
TextColor = color ?? SKColors.White;
// Auto-calculate width and height based on text
UpdateDimensions();
}
public override void Render(SKCanvas canvas, float globalOpacity = 1.0f)
{
if (!IsVisible || string.IsNullOrEmpty(Text))
return;
float effectiveOpacity = Opacity * globalOpacity;
using var typeface = SKTypeface.FromFamilyName(FontFamily, FontStyle);
using var paint = new SKPaint
{
Color = ApplyOpacity(TextColor, effectiveOpacity),
TextSize = FontSize,
Typeface = typeface,
TextAlign = TextAlign,
IsAntialias = IsAntialias
};
float textX = X;
float textY = Y + FontSize; // Baseline adjustment
// Adjust X position based on alignment
if (TextAlign == SKTextAlign.Center)
{
textX += Width / 2;
}
else if (TextAlign == SKTextAlign.Right)
{
textX += Width;
}
// Draw shadow if enabled
if (HasShadow)
{
using var shadowPaint = new SKPaint
{
Color = ApplyOpacity(ShadowColor, effectiveOpacity),
TextSize = FontSize,
Typeface = typeface,
TextAlign = TextAlign,
IsAntialias = IsAntialias
};
if (ShadowBlur > 0)
{
shadowPaint.MaskFilter = SKMaskFilter.CreateBlur(SKBlurStyle.Normal, ShadowBlur);
}
canvas.DrawText(Text, textX + ShadowOffsetX, textY + ShadowOffsetY, shadowPaint);
}
// Draw main text
canvas.DrawText(Text, textX, textY, paint);
}
/// <summary>
/// Update width and height based on current text and font settings
/// </summary>
public void UpdateDimensions()
{
if (string.IsNullOrEmpty(Text))
{
Width = 0;
Height = 0;
return;
}
using var typeface = SKTypeface.FromFamilyName(FontFamily, FontStyle);
using var paint = new SKPaint
{
TextSize = FontSize,
Typeface = typeface
};
var bounds = new SKRect();
paint.MeasureText(Text, ref bounds);
Width = bounds.Width;
Height = FontSize; // Use font size as height for consistency
}
}
}

View file

@ -14,4 +14,8 @@
<ProjectReference Include="..\Ryujinx.Graphics.Shader\Ryujinx.Graphics.Shader.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="SkiaSharp" />
</ItemGroup>
</Project>

View file

@ -1,10 +1,14 @@
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
@ -15,6 +19,8 @@ namespace Ryujinx.Graphics.Gpu
public class Window
{
private readonly GpuContext _context;
private readonly OverlayManager _overlayManager;
private DateTime _lastUpdateTime = DateTime.UtcNow;
/// <summary>
/// Texture presented on the window.
@ -98,8 +104,12 @@ namespace Ryujinx.Graphics.Gpu
public Window(GpuContext context)
{
_context = context;
_overlayManager = new OverlayManager();
_frameQueue = new ConcurrentQueue<PresentationTexture>();
// Initialize controller overlay
InitializeControllerOverlay();
}
/// <summary>
@ -207,6 +217,9 @@ namespace Ryujinx.Graphics.Gpu
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),
@ -244,6 +257,88 @@ namespace Ryujinx.Graphics.Gpu
}
}
/// <summary>
/// Initialize controller overlay
/// </summary>
private void InitializeControllerOverlay()
{
var controllerOverlay = new ControllerOverlay();
_overlayManager.AddOverlay(controllerOverlay);
}
/// <summary>
/// Add overlays to the texture using SkiaSharp
/// </summary>
/// <param name="texture">The texture to modify</param>
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);
// 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<byte>.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
System.Diagnostics.Debug.WriteLine($"Overlay rendering failed: {ex.Message}");
}
}
/// <summary>
/// Indicate that a frame on the queue is ready to be acquired.
/// </summary>
@ -267,5 +362,32 @@ namespace Ryujinx.Graphics.Gpu
return false;
}
/// <summary>
/// Show controller overlay with the provided input configurations
/// </summary>
/// <param name="inputConfigs">List of input configurations to display</param>
/// <param name="durationSeconds">Duration to show the overlay in seconds</param>
public void ShowControllerBindings(List<Common.Configuration.Hid.InputConfig> inputConfigs, int durationSeconds = 3)
{
var controllerOverlay = _overlayManager.FindOverlay("ControllerOverlay") as ControllerOverlay;
controllerOverlay?.ShowControllerBindings(inputConfigs, durationSeconds);
}
/// <summary>
/// Get the overlay manager for external access
/// </summary>
public OverlayManager GetOverlayManager()
{
return _overlayManager;
}
/// <summary>
/// Dispose resources
/// </summary>
public void Dispose()
{
_overlayManager?.Dispose();
}
}
}

View file

@ -1479,23 +1479,24 @@ namespace Ryujinx.Ava.Systems
private void ShowControllerOverlay(List<InputConfig> inputConfigs)
{
Dispatcher.UIThread.InvokeAsync(() =>
{
try
{
// Access the overlay through the MainWindow via the ViewModel
if (_viewModel?.Window?.ControllerOverlay != null)
// Show overlay through the GPU context window directly
if (Device?.Gpu?.Window != null)
{
int duration = ConfigurationState.Instance.ControllerOverlayInputCycleDuration.Value;
_viewModel.Window.ControllerOverlay.ShowControllerBindings(inputConfigs, duration);
Device.Gpu.Window.ShowControllerBindings(inputConfigs, duration);
}
else
{
Logger.Warning?.Print(LogClass.Application, "AppHost: Cannot show overlay - Device.Gpu.Window is null");
}
}
catch (Exception ex)
{
Logger.Error?.Print(LogClass.Application, $"Failed to show controller overlay: {ex.Message}");
}
});
}
private KeyboardHotkeyState GetHotkeyState()

View file

@ -1707,11 +1707,22 @@ namespace Ryujinx.Ava.UI.ViewModels
? ConfigurationState.InstanceExtra.Hid.InputConfig.Value
: ConfigurationState.Instance.Hid.InputConfig.Value;
// Only show overlay if there are actual controller configurations for players 1-4
if (inputConfigs?.Any(c => c.PlayerIndex <= PlayerIndex.Player4) == true)
// Debug: Log what we're getting
Logger.Info?.Print(LogClass.UI, $"MainWindowViewModel: Found {inputConfigs?.Count ?? 0} input configs");
if (inputConfigs != null)
{
foreach (var config in inputConfigs)
{
Logger.Info?.Print(LogClass.UI, $" Config: Player {config.PlayerIndex}, Backend {config.Backend}, Name '{config.Name}', Type {config.ControllerType}");
}
}
// Always show overlay - if no configs, it will show test data
int duration = ConfigurationState.Instance.ControllerOverlayGameStartDuration.Value;
Window.ControllerOverlay.ShowControllerBindings(inputConfigs, duration);
// Show overlay through the GPU context window directly
if (AppHost?.Device?.Gpu?.Window != null)
{
AppHost.Device.Gpu.Window.ShowControllerBindings(inputConfigs, duration);
}
}
catch (Exception ex)

View file

@ -180,9 +180,6 @@
Grid.Row="2" />
</Grid>
<!-- Controller Overlay -->
<overlayControls:ControllerOverlay
Name="ControllerOverlay"
ZIndex="2000" />
</Grid>
</window:StyleableAppWindow>