mirror of
https://git.ryujinx.app/ryubing/ryujinx.git
synced 2025-06-28 00:16:23 +02:00
Overlay system
This commit is contained in:
parent
3be20385ed
commit
069a703f22
13 changed files with 1088 additions and 19 deletions
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
244
src/Ryujinx.Graphics.Gpu/Overlay/ControllerOverlay.cs
Normal file
244
src/Ryujinx.Graphics.Gpu/Overlay/ControllerOverlay.cs
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
145
src/Ryujinx.Graphics.Gpu/Overlay/ImageElement.cs
Normal file
145
src/Ryujinx.Graphics.Gpu/Overlay/ImageElement.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
115
src/Ryujinx.Graphics.Gpu/Overlay/Overlay.cs
Normal file
115
src/Ryujinx.Graphics.Gpu/Overlay/Overlay.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
66
src/Ryujinx.Graphics.Gpu/Overlay/OverlayElement.cs
Normal file
66
src/Ryujinx.Graphics.Gpu/Overlay/OverlayElement.cs
Normal 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)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
160
src/Ryujinx.Graphics.Gpu/Overlay/OverlayManager.cs
Normal file
160
src/Ryujinx.Graphics.Gpu/Overlay/OverlayManager.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
78
src/Ryujinx.Graphics.Gpu/Overlay/RectangleElement.cs
Normal file
78
src/Ryujinx.Graphics.Gpu/Overlay/RectangleElement.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
121
src/Ryujinx.Graphics.Gpu/Overlay/TextElement.cs
Normal file
121
src/Ryujinx.Graphics.Gpu/Overlay/TextElement.cs
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -14,4 +14,8 @@
|
|||
<ProjectReference Include="..\Ryujinx.Graphics.Shader\Ryujinx.Graphics.Shader.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="SkiaSharp" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1480,22 +1480,23 @@ namespace Ryujinx.Ava.Systems
|
|||
|
||||
private void ShowControllerOverlay(List<InputConfig> inputConfigs)
|
||||
{
|
||||
Dispatcher.UIThread.InvokeAsync(() =>
|
||||
try
|
||||
{
|
||||
try
|
||||
// Show overlay through the GPU context window directly
|
||||
if (Device?.Gpu?.Window != null)
|
||||
{
|
||||
// Access the overlay through the MainWindow via the ViewModel
|
||||
if (_viewModel?.Window?.ControllerOverlay != null)
|
||||
{
|
||||
int duration = ConfigurationState.Instance.ControllerOverlayInputCycleDuration.Value;
|
||||
_viewModel.Window.ControllerOverlay.ShowControllerBindings(inputConfigs, duration);
|
||||
}
|
||||
int duration = ConfigurationState.Instance.ControllerOverlayInputCycleDuration.Value;
|
||||
Device.Gpu.Window.ShowControllerBindings(inputConfigs, duration);
|
||||
}
|
||||
catch (Exception ex)
|
||||
else
|
||||
{
|
||||
Logger.Error?.Print(LogClass.Application, $"Failed to show controller overlay: {ex.Message}");
|
||||
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()
|
||||
|
|
|
@ -1703,11 +1703,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)
|
||||
{
|
||||
int duration = ConfigurationState.Instance.ControllerOverlayGameStartDuration.Value;
|
||||
Window.ControllerOverlay.ShowControllerBindings(inputConfigs, duration);
|
||||
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;
|
||||
// Show overlay through the GPU context window directly
|
||||
if (AppHost?.Device?.Gpu?.Window != null)
|
||||
{
|
||||
AppHost.Device.Gpu.Window.ShowControllerBindings(inputConfigs, duration);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
|
|
@ -180,9 +180,6 @@
|
|||
Grid.Row="2" />
|
||||
</Grid>
|
||||
|
||||
<!-- Controller Overlay -->
|
||||
<overlayControls:ControllerOverlay
|
||||
Name="ControllerOverlay"
|
||||
ZIndex="2000" />
|
||||
|
||||
</Grid>
|
||||
</window:StyleableAppWindow>
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue