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
bfd715b607
commit
ba250df73d
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 SetScalingFilterLevel(float level) { }
|
||||||
|
|
||||||
public void SetColorSpacePassthrough(bool colorSpacePassthroughEnabled) { }
|
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" />
|
<ProjectReference Include="..\Ryujinx.Graphics.Shader\Ryujinx.Graphics.Shader.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="SkiaSharp" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|
|
@ -1,10 +1,14 @@
|
||||||
|
using Ryujinx.Common.Memory;
|
||||||
using Ryujinx.Graphics.GAL;
|
using Ryujinx.Graphics.GAL;
|
||||||
using Ryujinx.Graphics.Gpu.Image;
|
using Ryujinx.Graphics.Gpu.Image;
|
||||||
using Ryujinx.Graphics.Gpu.Memory;
|
using Ryujinx.Graphics.Gpu.Memory;
|
||||||
|
using Ryujinx.Graphics.Gpu.Overlay;
|
||||||
using Ryujinx.Graphics.Texture;
|
using Ryujinx.Graphics.Texture;
|
||||||
using Ryujinx.Memory.Range;
|
using Ryujinx.Memory.Range;
|
||||||
|
using SkiaSharp;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
|
|
||||||
namespace Ryujinx.Graphics.Gpu
|
namespace Ryujinx.Graphics.Gpu
|
||||||
|
@ -15,6 +19,8 @@ namespace Ryujinx.Graphics.Gpu
|
||||||
public class Window
|
public class Window
|
||||||
{
|
{
|
||||||
private readonly GpuContext _context;
|
private readonly GpuContext _context;
|
||||||
|
private readonly OverlayManager _overlayManager;
|
||||||
|
private DateTime _lastUpdateTime = DateTime.UtcNow;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Texture presented on the window.
|
/// Texture presented on the window.
|
||||||
|
@ -98,8 +104,12 @@ namespace Ryujinx.Graphics.Gpu
|
||||||
public Window(GpuContext context)
|
public Window(GpuContext context)
|
||||||
{
|
{
|
||||||
_context = context;
|
_context = context;
|
||||||
|
_overlayManager = new OverlayManager();
|
||||||
|
|
||||||
_frameQueue = new ConcurrentQueue<PresentationTexture>();
|
_frameQueue = new ConcurrentQueue<PresentationTexture>();
|
||||||
|
|
||||||
|
// Initialize controller overlay
|
||||||
|
InitializeControllerOverlay();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -207,6 +217,9 @@ namespace Ryujinx.Graphics.Gpu
|
||||||
|
|
||||||
texture.SynchronizeMemory();
|
texture.SynchronizeMemory();
|
||||||
|
|
||||||
|
// Add overlays by modifying texture data directly
|
||||||
|
AddOverlaysToTexture(texture);
|
||||||
|
|
||||||
ImageCrop crop = new(
|
ImageCrop crop = new(
|
||||||
(int)(pt.Crop.Left * texture.ScaleFactor),
|
(int)(pt.Crop.Left * texture.ScaleFactor),
|
||||||
(int)MathF.Ceiling(pt.Crop.Right * 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>
|
/// <summary>
|
||||||
/// Indicate that a frame on the queue is ready to be acquired.
|
/// Indicate that a frame on the queue is ready to be acquired.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -267,5 +362,32 @@ namespace Ryujinx.Graphics.Gpu
|
||||||
|
|
||||||
return false;
|
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)
|
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
|
int duration = ConfigurationState.Instance.ControllerOverlayInputCycleDuration.Value;
|
||||||
if (_viewModel?.Window?.ControllerOverlay != null)
|
Device.Gpu.Window.ShowControllerBindings(inputConfigs, duration);
|
||||||
{
|
|
||||||
int duration = ConfigurationState.Instance.ControllerOverlayInputCycleDuration.Value;
|
|
||||||
_viewModel.Window.ControllerOverlay.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()
|
private KeyboardHotkeyState GetHotkeyState()
|
||||||
|
|
|
@ -1707,11 +1707,22 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||||
? ConfigurationState.InstanceExtra.Hid.InputConfig.Value
|
? ConfigurationState.InstanceExtra.Hid.InputConfig.Value
|
||||||
: ConfigurationState.Instance.Hid.InputConfig.Value;
|
: ConfigurationState.Instance.Hid.InputConfig.Value;
|
||||||
|
|
||||||
// Only show overlay if there are actual controller configurations for players 1-4
|
// Debug: Log what we're getting
|
||||||
if (inputConfigs?.Any(c => c.PlayerIndex <= PlayerIndex.Player4) == true)
|
Logger.Info?.Print(LogClass.UI, $"MainWindowViewModel: Found {inputConfigs?.Count ?? 0} input configs");
|
||||||
|
if (inputConfigs != null)
|
||||||
{
|
{
|
||||||
int duration = ConfigurationState.Instance.ControllerOverlayGameStartDuration.Value;
|
foreach (var config in inputConfigs)
|
||||||
Window.ControllerOverlay.ShowControllerBindings(inputConfigs, duration);
|
{
|
||||||
|
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)
|
catch (Exception ex)
|
||||||
|
|
|
@ -180,9 +180,6 @@
|
||||||
Grid.Row="2" />
|
Grid.Row="2" />
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
<!-- Controller Overlay -->
|
|
||||||
<overlayControls:ControllerOverlay
|
|
||||||
Name="ControllerOverlay"
|
|
||||||
ZIndex="2000" />
|
|
||||||
</Grid>
|
</Grid>
|
||||||
</window:StyleableAppWindow>
|
</window:StyleableAppWindow>
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue