Mesen2/UI/Debugger/Utilities/ToolRefreshHelper.cs
Sour 86cac326d7 Debugger: Tile editor - Improved behavior when editing tiles from a tilemap in games that bankswitch vram in the middle of the screen
-Also allows cropping the edit window when the size doesn't fit (e.g using 4x4 near the bottom of the screen can result in a 4x2 edit window, etc.)
-Fixed a display issue when the number of edited columns & rows didn't match
2024-12-14 23:19:02 +09:00

239 lines
7.1 KiB
C#

using Avalonia.Controls;
using System;
using Mesen.Interop;
using Mesen.Config;
using System.Collections.Generic;
using System.Threading;
using Avalonia.Threading;
using System.Linq;
using Mesen.Debugger.ViewModels;
using Mesen.Debugger.Windows;
using System.ComponentModel;
using System.Runtime.InteropServices;
using System.Collections.Concurrent;
using System.Threading.Tasks;
namespace Mesen.Debugger.Utilities
{
public static class ToolRefreshHelper
{
class ToolInfo
{
internal int ViewerId { get; set; } = 0;
internal int Scanline { get; set; } = 0;
internal int Cycle { get; set; } = 0;
internal CpuType CpuType { get; set; }
}
public class LastRefreshInfo
{
public WeakReference<Window> Host;
public DateTime Stamp;
public LastRefreshInfo(Window host)
{
Host = new(host);
}
}
private static List<LastRefreshInfo> _lastRefreshStamp = new();
private static ConcurrentDictionary<Window, ToolInfo> _activeWindows = new();
private static int _nextId = 0;
public static void ExecuteAt(int scanline, int cycle, CpuType cpuType, Action callback)
{
//Execute callback at the specified scanline/cycle
//If the callback is not called by the core within 300ms
//call it from the UI as a fallback
object lockObject = new();
bool done = false;
int viewerId = ToolRefreshHelper.GetNextId();
NotificationListener? listener = new();
listener.OnNotification += (NotificationEventArgs e) => {
switch(e.NotificationType) {
case ConsoleNotificationType.ViewerRefresh:
if(e.Parameter.ToInt32() == viewerId) {
lock(callback) {
if(!done) {
Task.Run(() => {
//DebugApi.RemoveViewerId() must not be called inside the notification callback (which is
//the same thread as the emulation thread). Otherwise, the viewer timing collection the
//debugger is iterating on will be modified, causing a crash.
DebugApi.RemoveViewerId(viewerId, cpuType);
});
listener.Dispose();
listener = null;
callback();
done = true;
}
}
}
break;
}
};
DebugApi.SetViewerUpdateTiming(viewerId, scanline, cycle, cpuType);
Task.Run(async () => {
await Task.Delay(300);
if(listener != null) {
lock(callback) {
if(!done) {
//Give up after 300ms and call the callback
DebugApi.RemoveViewerId(viewerId, cpuType);
callback();
listener.Dispose();
done = true;
}
}
}
});
}
private static int GetNextId()
{
return Interlocked.Increment(ref _nextId);
}
private static int RegisterWindow(Window wnd, RefreshTimingConfig cfg, CpuType cpuType)
{
if(_activeWindows.ContainsKey(wnd)) {
throw new Exception("Register window called twice");
}
int newId = GetNextId();
wnd.Closing += OnWindowClosingHandler;
wnd.Closed += OnWindowClosedHandler;
_activeWindows.TryAdd(wnd, new ToolInfo() { ViewerId = newId, Scanline = cfg.RefreshScanline, Cycle = cfg.RefreshCycle, CpuType = cpuType });
DebugApi.SetViewerUpdateTiming(newId, cfg.RefreshScanline, cfg.RefreshCycle, cpuType);
return newId;
}
private static void OnWindowClosingHandler(object? sender, CancelEventArgs e)
{
if(sender is Window wnd) {
//Do this in OnClosing (before OnClosed), to call RemoveViewerId before the debugger gets
//shut down by DebugWindowManager when closing the last debug tool window
if(_activeWindows.TryGetValue(wnd, out ToolInfo? toolInfo)) {
DebugApi.RemoveViewerId(toolInfo.ViewerId, toolInfo.CpuType);
}
wnd.Closing -= OnWindowClosingHandler;
}
}
private static void OnWindowClosedHandler(object? sender, EventArgs e)
{
if(sender is Window wnd) {
//Do this in OnClosed to ensure the window doesn't get re-registered by a
//notification received between the Closing and Closed events
_activeWindows.TryRemove(wnd, out _);
wnd.Closed -= OnWindowClosedHandler;
}
}
private static int GetViewerId(Window wnd, RefreshTimingConfig cfg, CpuType cpuType)
{
if(_activeWindows.TryGetValue(wnd, out ToolInfo? toolInfo)) {
if(cfg.RefreshScanline != toolInfo.Scanline || cfg.RefreshCycle != toolInfo.Cycle) {
toolInfo.Scanline = cfg.RefreshScanline;
toolInfo.Cycle = cfg.RefreshCycle;
toolInfo.CpuType = cpuType;
DebugApi.SetViewerUpdateTiming(toolInfo.ViewerId, cfg.RefreshScanline, cfg.RefreshCycle, cpuType);
}
return toolInfo.ViewerId;
} else {
lock(_activeWindows) {
//Lock to prevent registering the same window twice in 2 separate threads
if(!_activeWindows.TryGetValue(wnd, out toolInfo)) {
return RegisterWindow(wnd, cfg, cpuType);
} else {
return toolInfo.ViewerId;
}
}
}
}
public static void ProcessNotification(Window wnd, NotificationEventArgs e, RefreshTimingViewModel cfg, ICpuTypeModel model, Action refresh)
{
int viewerId = GetViewerId(wnd, cfg.Config, model.CpuType);
switch(e.NotificationType) {
case ConsoleNotificationType.ViewerRefresh:
if(cfg.Config.AutoRefresh && e.Parameter.ToInt32() == viewerId && !LimitFps(wnd, 80)) {
refresh();
}
break;
case ConsoleNotificationType.StateLoaded:
refresh();
break;
case ConsoleNotificationType.CodeBreak:
if(cfg.Config.RefreshOnBreakPause) {
refresh();
}
break;
case ConsoleNotificationType.GameLoaded:
GameLoadedEventParams evtParams = Marshal.PtrToStructure<GameLoadedEventParams>(e.Parameter);
if(!evtParams.IsPowerCycle) {
//When loading a game from disk, tools need to be updated (cpu type, etc.)
RomInfo romInfo = EmuApi.GetRomInfo();
if(!romInfo.CpuTypes.Contains(model.CpuType)) {
model.CpuType = romInfo.ConsoleType.GetMainCpuType();
}
model.OnGameLoaded();
Dispatcher.UIThread.Post(() => {
cfg.UpdateMinMaxValues(model.CpuType);
});
}
if(_activeWindows.TryGetValue(wnd, out ToolInfo? toolInfo)) {
toolInfo.CpuType = model.CpuType;
DebugApi.SetViewerUpdateTiming(toolInfo.ViewerId, cfg.Config.RefreshScanline, cfg.Config.RefreshCycle, model.CpuType);
}
break;
}
}
public static bool LimitFps(Window wnd, int maxFps)
{
if(_lastRefreshStamp.Count > 5) {
//Reduce FPS by 10% for each active tool above 5 (down to 30% refresh speed)
maxFps = Math.Max(5, (int)(maxFps * Math.Max(0.3, 1.0 - (_lastRefreshStamp.Count - 5) * 0.1)));
}
DateTime now = DateTime.Now;
for(int i = _lastRefreshStamp.Count - 1; i >= 0; i--) {
if(_lastRefreshStamp[i].Host.TryGetTarget(out Window? target)) {
if(!DebugWindowManager.IsDebugWindow(target)) {
//Window was closed, remove from list
_lastRefreshStamp.RemoveAt(i);
continue;
}
if(target == wnd) {
if((now - _lastRefreshStamp[i].Stamp).TotalMilliseconds < 1000.0 / maxFps) {
return true;
} else {
_lastRefreshStamp[i].Stamp = now;
return false;
}
}
} else {
_lastRefreshStamp.RemoveAt(i);
}
}
_lastRefreshStamp.Add(new LastRefreshInfo(wnd) { Stamp = now });
return false;
}
}
}